From 8e252ac6394be84284876bf71e30b92b4bf5e50f Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 09:57:15 +0900 Subject: [PATCH 01/25] dotnet format --- Processor.Tests/InstructionPointerTests.cs | 68 +++++++------- Processor.Tests/StackStackTests.cs | 102 ++++++++++----------- 2 files changed, 85 insertions(+), 85 deletions(-) diff --git a/Processor.Tests/InstructionPointerTests.cs b/Processor.Tests/InstructionPointerTests.cs index a5f3ab8..f1a5170 100644 --- a/Processor.Tests/InstructionPointerTests.cs +++ b/Processor.Tests/InstructionPointerTests.cs @@ -1,34 +1,34 @@ -using Esolang.Funge.Parser; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Esolang.Funge.Processor.Tests; - -[TestClass] -public class InstructionPointerTests -{ - [TestMethod] - public void CreateChild_CopiesStateCorrectly() - { - var parent = new InstructionPointer(1) - { - Position = new FungeVector(1, 2, 3), - Delta = new FungeVector(0, 1, 0), - Offset = new FungeVector(10, 10, 10), - StringMode = true - }; - parent.StackStack.Push(42); - - var child = parent.CreateChild(2); - - Assert.AreEqual(2, child.Id); - Assert.AreEqual(parent.Position, child.Position); - Assert.AreNotEqual(parent.Delta, child.Delta); // Should be reflected - Assert.AreEqual(parent.Offset, child.Offset); - Assert.AreEqual(parent.StringMode, child.StringMode); - - // Stack should be cloned - Assert.AreEqual(42, child.StackStack.Pop()); - child.StackStack.Push(99); - Assert.AreEqual(42, parent.StackStack.Pop()); // Original unaffected - } -} +using Esolang.Funge.Parser; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Esolang.Funge.Processor.Tests; + +[TestClass] +public class InstructionPointerTests +{ + [TestMethod] + public void CreateChild_CopiesStateCorrectly() + { + var parent = new InstructionPointer(1) + { + Position = new FungeVector(1, 2, 3), + Delta = new FungeVector(0, 1, 0), + Offset = new FungeVector(10, 10, 10), + StringMode = true + }; + parent.StackStack.Push(42); + + var child = parent.CreateChild(2); + + Assert.AreEqual(2, child.Id); + Assert.AreEqual(parent.Position, child.Position); + Assert.AreNotEqual(parent.Delta, child.Delta); // Should be reflected + Assert.AreEqual(parent.Offset, child.Offset); + Assert.AreEqual(parent.StringMode, child.StringMode); + + // Stack should be cloned + Assert.AreEqual(42, child.StackStack.Pop()); + child.StackStack.Push(99); + Assert.AreEqual(42, parent.StackStack.Pop()); // Original unaffected + } +} diff --git a/Processor.Tests/StackStackTests.cs b/Processor.Tests/StackStackTests.cs index 010c976..fa63105 100644 --- a/Processor.Tests/StackStackTests.cs +++ b/Processor.Tests/StackStackTests.cs @@ -1,51 +1,51 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Esolang.Funge.Processor.Tests; - -[TestClass] -public class StackStackTests -{ - [TestMethod] - public void PushPop_MaintainsLIFO() - { - var ss = new StackStack(); - ss.Push(1); - ss.Push(2); - Assert.AreEqual(2, ss.Pop()); - Assert.AreEqual(1, ss.Pop()); - Assert.AreEqual(0, ss.Pop()); // Empty returns 0 - } - - [TestMethod] - public void StackStackOperations_ManageStacksCorrectly() - { - var ss = new StackStack(); - ss.Push(1); - ss.PushNewStack(); - ss.Push(2); - - Assert.AreEqual(2, ss.TOSS.Peek()); - Assert.IsTrue(ss.HasSOSS); - Assert.AreEqual(2, ss.StackCount); - - ss.PopCurrentStack(); - Assert.AreEqual(1, ss.TOSS.Peek()); - Assert.IsFalse(ss.HasSOSS); - Assert.AreEqual(1, ss.StackCount); - } - - [TestMethod] - public void Clone_CreatesDeepCopy() - { - var ss = new StackStack(); - ss.Push(1); - ss.PushNewStack(); - ss.Push(2); - - var clone = ss.Clone(); - Assert.AreEqual(2, clone.Pop()); - - // Ensure original is unaffected - Assert.AreEqual(2, ss.TOSS.Peek()); - } -} +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Esolang.Funge.Processor.Tests; + +[TestClass] +public class StackStackTests +{ + [TestMethod] + public void PushPop_MaintainsLIFO() + { + var ss = new StackStack(); + ss.Push(1); + ss.Push(2); + Assert.AreEqual(2, ss.Pop()); + Assert.AreEqual(1, ss.Pop()); + Assert.AreEqual(0, ss.Pop()); // Empty returns 0 + } + + [TestMethod] + public void StackStackOperations_ManageStacksCorrectly() + { + var ss = new StackStack(); + ss.Push(1); + ss.PushNewStack(); + ss.Push(2); + + Assert.AreEqual(2, ss.TOSS.Peek()); + Assert.IsTrue(ss.HasSOSS); + Assert.AreEqual(2, ss.StackCount); + + ss.PopCurrentStack(); + Assert.AreEqual(1, ss.TOSS.Peek()); + Assert.IsFalse(ss.HasSOSS); + Assert.AreEqual(1, ss.StackCount); + } + + [TestMethod] + public void Clone_CreatesDeepCopy() + { + var ss = new StackStack(); + ss.Push(1); + ss.PushNewStack(); + ss.Push(2); + + var clone = ss.Clone(); + Assert.AreEqual(2, clone.Pop()); + + // Ensure original is unaffected + Assert.AreEqual(2, ss.TOSS.Peek()); + } +} From 6239b67fc7815ffb0ba7046f0ba2e12bb825e4ac Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 10:06:38 +0900 Subject: [PATCH 02/25] feat(generator): add FG0011 partial method constraint --- CHANGELOG.md | 3 ++ .../PartialMethodConstraintTests.cs | 37 +++++++++++++++ Generator/AnalyzerReleases.Unshipped.md | 3 +- Generator/DiagnosticDescriptors.cs | 11 +++++ Generator/MethodGenerator.cs | 47 +++++++++++-------- 5 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 Generator.Tests/PartialMethodConstraintTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 435ef60..2ce5546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on Keep a Changelog. ## [Unreleased] +### Added +- `Esolang.Funge.Generator`: Added FG0011 diagnostic for enforcing partial method declaration. + ## [1.1.1] - 2026-05-25 - `Esolang.Funge.Generator`: Implement logging support for runtime instructions and events (instruction execution, fingerprint operations). diff --git a/Generator.Tests/PartialMethodConstraintTests.cs b/Generator.Tests/PartialMethodConstraintTests.cs new file mode 100644 index 0000000..6a57487 --- /dev/null +++ b/Generator.Tests/PartialMethodConstraintTests.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Esolang.Funge.Generator.Tests; + +[TestClass] +public class PartialMethodConstraintTests(TestContext TestContext) +{ + +#pragma warning disable MSTEST0054 // TestContext.CancellationTokenSource.Token の代わりに TestContext.CancellationToken を使用する + CancellationToken Cancellationtoken => TestContext.CancellationTokenSource.Token; +#pragma warning restore MSTEST0054 // TestContext.CancellationTokenSource.Token の代わりに TestContext.CancellationToken を使用する + [TestMethod] + public void Generator_NonPartialMethod_ReportsError() + { + const string source = """ + namespace Demo; + + public class Sample + { + [Esolang.Funge.GenerateFungeMethod(InlineSource = ">@")] + public void RunSync() { } + } + """; + + var compilation = CSharpCompilation.Create("Test", + [CSharpSyntaxTree.ParseText(source, cancellationToken: Cancellationtoken)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + + var generator = new MethodGenerator(); + var driver = CSharpGeneratorDriver.Create(generator); + driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out var diagnostics, Cancellationtoken); + + Assert.Contains(d => d.Id == "FG0011", diagnostics, "Expected diagnostic FG0011 (Method must be partial)"); + } +} diff --git a/Generator/AnalyzerReleases.Unshipped.md b/Generator/AnalyzerReleases.Unshipped.md index 6a2a74b..7c80f9f 100644 --- a/Generator/AnalyzerReleases.Unshipped.md +++ b/Generator/AnalyzerReleases.Unshipped.md @@ -1,4 +1,5 @@ ### New Rules Rule ID | Category | Severity | Notes ---------|----------|----------|-------------------- +--------|----------|----------|--------- +FG0011 | Funge | Error | Method must be partial.----------- diff --git a/Generator/DiagnosticDescriptors.cs b/Generator/DiagnosticDescriptors.cs index 47905d6..079d55c 100644 --- a/Generator/DiagnosticDescriptors.cs +++ b/Generator/DiagnosticDescriptors.cs @@ -118,4 +118,15 @@ public static class DiagnosticDescriptors category: Category, defaultSeverity: DiagnosticSeverity.Hidden, isEnabledByDefault: true); + + /// + /// FG0011: Method must be partial. + /// + public static readonly DiagnosticDescriptor MethodMustBePartial = new( + id: "FG0011", + title: "Method must be partial", + messageFormat: "The method '{0}' must be declared as 'partial'", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); } diff --git a/Generator/MethodGenerator.cs b/Generator/MethodGenerator.cs index 1029744..6ee0ffc 100644 --- a/Generator/MethodGenerator.cs +++ b/Generator/MethodGenerator.cs @@ -82,7 +82,7 @@ public KnownTypes(Compilation compilation) String = compilation.GetSpecialType(SpecialType.System_String); var byteSymbol = compilation.GetSpecialType(SpecialType.System_Byte); var intSymbol = compilation.GetSpecialType(SpecialType.System_Int32); - + var taskGeneric = GetBestTypeByMetadataName(compilation, "System.Threading.Tasks.Task`1"); Task = GetBestTypeByMetadataName(compilation, "System.Threading.Tasks.Task"); TaskInt = taskGeneric?.Construct(intSymbol); @@ -235,6 +235,15 @@ internal sealed class {{AttributeName}} : Attribute var method = (MethodDeclarationSyntax)syntaxCtx.TargetNode; var symbol = (IMethodSymbol)syntaxCtx.TargetSymbol; + if (!symbol.IsPartialDefinition) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.MethodMustBePartial, + method.Identifier.GetLocation(), + symbol.Name)); + continue; + } + if (!IsLanguageVersionAtLeastCSharp8(langVersion)) { ctx.ReportDiagnostic(Diagnostic.Create( @@ -276,20 +285,20 @@ internal sealed class {{AttributeName}} : Attribute break; } } - + if (inlineSource == null) { - var attributeSyntax = attrData.ApplicationSyntaxReference?.GetSyntax() as AttributeSyntax; - if (attributeSyntax?.ArgumentList != null) - { - foreach (var arg in attributeSyntax.ArgumentList.Arguments) - { - if (arg.NameEquals?.Name.Identifier.ValueText == "InlineSource" && arg.Expression is LiteralExpressionSyntax lit) - { - inlineSource = lit.Token.ValueText; - } - } - } + var attributeSyntax = attrData.ApplicationSyntaxReference?.GetSyntax() as AttributeSyntax; + if (attributeSyntax?.ArgumentList != null) + { + foreach (var arg in attributeSyntax.ArgumentList.Arguments) + { + if (arg.NameEquals?.Name.Identifier.ValueText == "InlineSource" && arg.Expression is LiteralExpressionSyntax lit) + { + inlineSource = lit.Token.ValueText; + } + } + } } if (string.IsNullOrWhiteSpace(inlineSource) && string.IsNullOrWhiteSpace(sourcePath)) @@ -461,13 +470,13 @@ static ExecutionBinding BindExecutionSignature(IMethodSymbol method, MethodDecla if (returnKind == ReturnKind.Invalid) { - // returnKind is invalid, check types for debugging - // Using a diagnostic for debugging as we are in a generator - // ctx.ReportDiagnostic(...); // Need context here, but BindExecutionSignature doesn't have it. - // We'll have to return an error diagnostic later in Initialize. - // For now, let's keep returnKind as invalid to trigger the error. + // returnKind is invalid, check types for debugging + // Using a diagnostic for debugging as we are in a generator + // ctx.ReportDiagnostic(...); // Need context here, but BindExecutionSignature doesn't have it. + // We'll have to return an error diagnostic later in Initialize. + // For now, let's keep returnKind as invalid to trigger the error. } - + if (returnKind == ReturnKind.Invalid) return new(false, returnKind, InputKind.None, OutputKind.None, "", "", null, null, null, null, false, DiagnosticDescriptors.InvalidReturnType.Id); From 44f36fa69552e265c61cbdb27d34931215305d6c Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 10:07:44 +0900 Subject: [PATCH 03/25] dotnet format --- Generator/MethodGenerator.Runtime.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index 3e2881b..b0dd5b6 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -9,16 +9,16 @@ partial class MethodGenerator [Flags] enum RuntimeFacadeFeatures { - None = 0, - RunSync = 1 << 0, - RunString = 1 << 1, - RunEnumerable = 1 << 2, + None = 0, + RunSync = 1 << 0, + RunString = 1 << 1, + RunEnumerable = 1 << 2, RunAsyncEnumerable = 1 << 3, - RunTask = 1 << 4, - RunTaskInt = 1 << 5, - RunTaskString = 1 << 6, + RunTask = 1 << 4, + RunTaskInt = 1 << 5, + RunTaskString = 1 << 6, RunValueTask = 1 << 7, - RunValueTaskInt = 1 << 8, + RunValueTaskInt = 1 << 8, RunValueTaskString = 1 << 9, RunWithLogging = 1 << 10, } From f35dd67e7d19dfc6d70cd5843e3231f50af7fa54 Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 10:35:51 +0900 Subject: [PATCH 04/25] docs: add diagnostic rule descriptions for Funge generator --- Generator/Rules/FG0001.md | 18 ++++++++++++++++++ Generator/Rules/FG0002.md | 18 ++++++++++++++++++ Generator/Rules/FG0003.md | 18 ++++++++++++++++++ Generator/Rules/FG0004.md | 18 ++++++++++++++++++ Generator/Rules/FG0005.md | 7 +++++++ Generator/Rules/FG0006.md | 18 ++++++++++++++++++ Generator/Rules/FG0007.md | 18 ++++++++++++++++++ Generator/Rules/FG0008.md | 18 ++++++++++++++++++ Generator/Rules/FG0009.md | 18 ++++++++++++++++++ Generator/Rules/FG0010.md | 18 ++++++++++++++++++ Generator/Rules/FG0011.md | 18 ++++++++++++++++++ 11 files changed, 187 insertions(+) create mode 100644 Generator/Rules/FG0001.md create mode 100644 Generator/Rules/FG0002.md create mode 100644 Generator/Rules/FG0003.md create mode 100644 Generator/Rules/FG0004.md create mode 100644 Generator/Rules/FG0005.md create mode 100644 Generator/Rules/FG0006.md create mode 100644 Generator/Rules/FG0007.md create mode 100644 Generator/Rules/FG0008.md create mode 100644 Generator/Rules/FG0009.md create mode 100644 Generator/Rules/FG0010.md create mode 100644 Generator/Rules/FG0011.md diff --git a/Generator/Rules/FG0001.md b/Generator/Rules/FG0001.md new file mode 100644 index 0000000..1030170 --- /dev/null +++ b/Generator/Rules/FG0001.md @@ -0,0 +1,18 @@ +# FG0001: Invalid source path parameter + +## Cause +The `sourcePath` provided to the `GenerateFungeMethodAttribute` is null or empty. + +## Solution +Ensure a valid, non-empty source path or inline source is provided to the attribute. + +## Example + +```cs +partial class Sample +{ + // Incorrect: Missing or empty source + [GenerateFungeMethod("")] + public static partial void Invalid(); +} +``` diff --git a/Generator/Rules/FG0002.md b/Generator/Rules/FG0002.md new file mode 100644 index 0000000..9abd063 --- /dev/null +++ b/Generator/Rules/FG0002.md @@ -0,0 +1,18 @@ +# FG0002: Unsupported return type + +## Cause +The method's return type is not supported by the Funge source generator. + +## Solution +Change the return type to one of the supported types (e.g., `void`, `int`, `string`, `Task`, `ValueTask`, `IEnumerable`, `IAsyncEnumerable`). + +## Example + +```cs +partial class Sample +{ + // Incorrect: double is not a supported return type + [GenerateFungeMethod(InlineSource = ">@")] + public static partial double Invalid(); +} +``` diff --git a/Generator/Rules/FG0003.md b/Generator/Rules/FG0003.md new file mode 100644 index 0000000..0ce0d43 --- /dev/null +++ b/Generator/Rules/FG0003.md @@ -0,0 +1,18 @@ +# FG0003: Unsupported parameter type + +## Cause +The method contains a parameter of an unsupported type. + +## Solution +Ensure all parameters are of a supported type (e.g., `string`, `TextReader`, `PipeReader`, `TextWriter`, `PipeWriter`, `CancellationToken`, `ILogger`). + +## Example + +```cs +partial class Sample +{ + // Incorrect: int is not a supported parameter type + [GenerateFungeMethod(InlineSource = ">@")] + public static partial void Invalid(int param); +} +``` diff --git a/Generator/Rules/FG0004.md b/Generator/Rules/FG0004.md new file mode 100644 index 0000000..0628a73 --- /dev/null +++ b/Generator/Rules/FG0004.md @@ -0,0 +1,18 @@ +# FG0004: Funge source file not found + +## Cause +The Funge source file path provided to the `GenerateFungeMethodAttribute` cannot be resolved. + +## Solution +Ensure the source file exists and is correctly referenced in the project's `AdditionalFiles`. + +## Example + +```cs +partial class Sample +{ + // Incorrect: "nonexistent.b98" is not found + [GenerateFungeMethod("nonexistent.b98")] + public static partial void Invalid(); +} +``` diff --git a/Generator/Rules/FG0005.md b/Generator/Rules/FG0005.md new file mode 100644 index 0000000..30a623c --- /dev/null +++ b/Generator/Rules/FG0005.md @@ -0,0 +1,7 @@ +# FG0005: Language version too low + +## Cause +The consumer language version is below C# 8.0, which is required for Funge source generation. + +## Solution +Update the C# language version to 8.0 or later. diff --git a/Generator/Rules/FG0006.md b/Generator/Rules/FG0006.md new file mode 100644 index 0000000..512d990 --- /dev/null +++ b/Generator/Rules/FG0006.md @@ -0,0 +1,18 @@ +# FG0006: Duplicate parameter type + +## Cause +The method contains multiple parameters of the same type or conflicting types. + +## Solution +Ensure each supported parameter type is only used once per method. + +## Example + +```cs +partial class Sample +{ + // Incorrect: duplicate CancellationToken parameter + [GenerateFungeMethod(InlineSource = ">@")] + public static partial void Invalid(System.Threading.CancellationToken ct1, System.Threading.CancellationToken ct2); +} +``` diff --git a/Generator/Rules/FG0007.md b/Generator/Rules/FG0007.md new file mode 100644 index 0000000..67a78c6 --- /dev/null +++ b/Generator/Rules/FG0007.md @@ -0,0 +1,18 @@ +# FG0007: Return type and output parameter conflict + +## Cause +Method has both return-based output and an output parameter (TextWriter/PipeWriter). + +## Solution +Use one or the other, not both. + +## Example + +```cs +partial class Sample +{ + // Incorrect: string return and TextWriter output parameter conflict + [GenerateFungeMethod(InlineSource = ".@")] + public static partial string Invalid(System.IO.TextWriter output); +} +``` diff --git a/Generator/Rules/FG0008.md b/Generator/Rules/FG0008.md new file mode 100644 index 0000000..f63b7b8 --- /dev/null +++ b/Generator/Rules/FG0008.md @@ -0,0 +1,18 @@ +# FG0008: Output interface required + +## Cause +Method uses Funge output instructions but has no output mechanism defined. + +## Solution +Add a return type (string/IEnumerable<byte>) or a TextWriter/PipeWriter parameter. + +## Example + +```cs +partial class Sample +{ + // Incorrect: requires output but no output mechanism + [GenerateFungeMethod(InlineSource = ".@")] + public static partial void Invalid(); +} +``` diff --git a/Generator/Rules/FG0009.md b/Generator/Rules/FG0009.md new file mode 100644 index 0000000..e940881 --- /dev/null +++ b/Generator/Rules/FG0009.md @@ -0,0 +1,18 @@ +# FG0009: Input interface required + +## Cause +Method uses Funge input instructions but has no input mechanism defined. + +## Solution +Add an input parameter (string, TextReader, or PipeReader). + +## Example + +```cs +partial class Sample +{ + // Incorrect: requires input but no input mechanism + [GenerateFungeMethod(InlineSource = ",@")] + public static partial void Invalid(); +} +``` diff --git a/Generator/Rules/FG0010.md b/Generator/Rules/FG0010.md new file mode 100644 index 0000000..f15270b --- /dev/null +++ b/Generator/Rules/FG0010.md @@ -0,0 +1,18 @@ +# FG0010: Unused input interface + +## Cause +Method has an input parameter but the Funge source does not use any input instructions. + +## Solution +Remove the unused input parameter. + +## Example + +```cs +partial class Sample +{ + // Incorrect: input instruction not used + [GenerateFungeMethod(InlineSource = ">@")] + public static partial void Invalid(string input); +} +``` diff --git a/Generator/Rules/FG0011.md b/Generator/Rules/FG0011.md new file mode 100644 index 0000000..93e2c37 --- /dev/null +++ b/Generator/Rules/FG0011.md @@ -0,0 +1,18 @@ +# FG0011: Method must be partial + +## Cause +The method decorated with `GenerateFungeMethodAttribute` is not declared with the `partial` modifier. + +## Solution +Add the `partial` modifier to the method definition. + +## Example + +```cs +partial class Sample +{ + // Incorrect: not partial + [GenerateFungeMethod(InlineSource = ">@")] + public void Invalid(); +} +``` From 87fc8aaf1906dfdaa087a046c12c073ba16ae8eb Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 13:13:48 +0900 Subject: [PATCH 05/25] docs: add rule links to Funge README --- Generator/README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Generator/README.md b/Generator/README.md index b1b82e5..96385f1 100644 --- a/Generator/README.md +++ b/Generator/README.md @@ -144,16 +144,17 @@ public partial class MyPrograms(ILogger logger) | ID | Severity | Description | | --- | --- | --- | -| FG0001 | Error | `sourcePath` is empty and `InlineSource` is not set | -| FG0002 | Error | Unsupported return type | -| FG0003 | Error | Unsupported parameter type | -| FG0004 | Error | Source file not found in `AdditionalFiles` | -| FG0005 | Warning | C# language version is too low (requires ≥ C# 8) | -| FG0006 | Error | Duplicate input/output parameter | -| FG0007 | Error | Return type conflicts with explicit output parameter | -| FG0008 | Info | Program appears to use output (`.`/`,`) but no output parameter or output return type is declared (static best-effort scan; runtime throws if reached) | -| FG0009 | Info | Program appears to use input (`&`/`~`) but no input parameter is declared (static best-effort scan; runtime throws if reached) | -| FG0010 | Hidden | Input parameter declared but program never reads input | +| [FG0001](Rules/FG0001.md) | Error | `sourcePath` is empty and `InlineSource` is not set | +| [FG0002](Rules/FG0002.md) | Error | Unsupported return type | +| [FG0003](Rules/FG0003.md) | Error | Unsupported parameter type | +| [FG0004](Rules/FG0004.md) | Error | Source file not found in `AdditionalFiles` | +| [FG0005](Rules/FG0005.md) | Warning | C# language version is too low (requires ≥ C# 8) | +| [FG0006](Rules/FG0006.md) | Error | Duplicate input/output parameter | +| [FG0007](Rules/FG0007.md) | Error | Return type conflicts with explicit output parameter | +| [FG0008](Rules/FG0008.md) | Info | Program appears to use output (`.`/`,`) but no output parameter or output return type is declared (static best-effort scan; runtime throws if reached) | +| [FG0009](Rules/FG0009.md) | Info | Program appears to use input (`&`/`~`) but no input parameter is declared (static best-effort scan; runtime throws if reached) | +| [FG0010](Rules/FG0010.md) | Hidden | Input parameter declared but program never reads input | +| [FG0011](Rules/FG0011.md) | Error | Method must be partial | ## Funge-98 Compliance From a6490fdcb97c81468faa80ddcabdf7bc28b043b2 Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 13:16:36 +0900 Subject: [PATCH 06/25] build: include Rules/*.md in nuget package --- Generator/Esolang.Funge.Generator.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Generator/Esolang.Funge.Generator.csproj b/Generator/Esolang.Funge.Generator.csproj index 794d4c9..122bd6e 100644 --- a/Generator/Esolang.Funge.Generator.csproj +++ b/Generator/Esolang.Funge.Generator.csproj @@ -25,6 +25,7 @@ + From 097e112f88f14da8c87195008ff35701342ae754 Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 17:50:14 +0900 Subject: [PATCH 07/25] refactor(funge-interpreter): convert to top-level statements and add test --- Interpreter.Tests/ProgramTests.cs | 49 ++++--------------------------- Interpreter/Program.cs | 42 +++++++++++++------------- 2 files changed, 25 insertions(+), 66 deletions(-) diff --git a/Interpreter.Tests/ProgramTests.cs b/Interpreter.Tests/ProgramTests.cs index 22cf736..0f09962 100644 --- a/Interpreter.Tests/ProgramTests.cs +++ b/Interpreter.Tests/ProgramTests.cs @@ -1,56 +1,17 @@ -using Esolang.Funge.Interpreter; using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Reflection; namespace Esolang.Funge.Interpreter.Tests; [TestClass] public class ProgramTests { - const string HelloWorldProgram = "64+\"!dlroW ,olleH\">:#,_@"; - [TestMethod] - public async Task RunAsync_HelpOption_ReturnsZero() + public void EntryPoint_Invoke_ReturnsZero() { - var exitCode = await Program.RunAsync(["--help"]); + var entryPoint = typeof(Program).Assembly.EntryPoint!; + var task = (Task)entryPoint.Invoke(null, new object[] { Array.Empty() })!; + var exitCode = task.Result; Assert.AreEqual(0, exitCode); } - - [TestMethod] - public async Task RunAsync_HelloWorld_ReturnsZero() - { - var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.b98"); - try - { - await File.WriteAllTextAsync(path, HelloWorldProgram); - - var exitCode = await Program.RunAsync([path]); - Assert.AreEqual(0, exitCode); - } - finally - { - if (File.Exists(path)) - File.Delete(path); - } - } - - [TestMethod] - public async Task RunAsync_CancelledToken_StopsInfiniteProgram() - { - var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.b98"); - try - { - await File.WriteAllTextAsync(path, ">"); - - using var cancellation = new CancellationTokenSource(); - cancellation.Cancel(); - - var exitCode = await Program.RunAsync([path], cancellation.Token); - Assert.AreEqual(0, exitCode); - } - finally - { - if (File.Exists(path)) - File.Delete(path); - } - } } diff --git a/Interpreter/Program.cs b/Interpreter/Program.cs index 4c0934d..a635735 100644 --- a/Interpreter/Program.cs +++ b/Interpreter/Program.cs @@ -1,9 +1,28 @@ +using Esolang.Funge.Interpreter; + +using var cancellation = new CancellationTokenSource(); +void OnCancelKeyPress(object? _, ConsoleCancelEventArgs e) +{ + e.Cancel = true; + cancellation.Cancel(); +} + +Console.CancelKeyPress += OnCancelKeyPress; +try +{ + return await Program.RunAsync(args, cancellation.Token); +} +finally +{ + Console.CancelKeyPress -= OnCancelKeyPress; +} + namespace Esolang.Funge.Interpreter; /// /// Entry point for the dotnet-funge command-line tool. /// -public static class Program +public partial class Program { /// /// Runs the command-line pipeline and returns the process exit code. @@ -16,25 +35,4 @@ public static async Task RunAsync(string[] args, CancellationToken cancella var rootCommand = FungeInterpreterExtensions.BuildRootCommand(); return await rootCommand.Parse(args).InvokeAsync(cancellationToken: cancellationToken); } - - /// Application entry point. - public static async Task Main(string[] args) - { - using var cancellation = new CancellationTokenSource(); - void OnCancelKeyPress(object? _, ConsoleCancelEventArgs e) - { - e.Cancel = true; - cancellation.Cancel(); - } - - Console.CancelKeyPress += OnCancelKeyPress; - try - { - return await RunAsync(args, cancellation.Token); - } - finally - { - Console.CancelKeyPress -= OnCancelKeyPress; - } - } } From 4fa35246342657da32eaa1554be2c29b5c74688c Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 17:51:32 +0900 Subject: [PATCH 08/25] fix(funge-interpreter): restore original test and add entrypoint test --- Interpreter.Tests/ProgramTests.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Interpreter.Tests/ProgramTests.cs b/Interpreter.Tests/ProgramTests.cs index 0f09962..c1edc78 100644 --- a/Interpreter.Tests/ProgramTests.cs +++ b/Interpreter.Tests/ProgramTests.cs @@ -1,11 +1,18 @@ +using Esolang.Funge.Interpreter; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Reflection; namespace Esolang.Funge.Interpreter.Tests; [TestClass] public class ProgramTests { + [TestMethod] + public async Task RunAsync_Default_ReturnsZero() + { + var exitCode = await Program.RunAsync(Array.Empty(), CancellationToken.None); + Assert.AreEqual(0, exitCode); + } + [TestMethod] public void EntryPoint_Invoke_ReturnsZero() { From 467182cc043d53bab5a210b45721519b92b607c1 Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 18:32:53 +0900 Subject: [PATCH 09/25] fix(funge-interpreter): update tests to use static Run helper --- Interpreter.Tests/ProgramTests.cs | 64 +++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/Interpreter.Tests/ProgramTests.cs b/Interpreter.Tests/ProgramTests.cs index c1edc78..e02d41b 100644 --- a/Interpreter.Tests/ProgramTests.cs +++ b/Interpreter.Tests/ProgramTests.cs @@ -4,21 +4,71 @@ namespace Esolang.Funge.Interpreter.Tests; [TestClass] -public class ProgramTests +public class ProgramTests(TestContext TestContext) { +#pragma warning disable MSTEST0054 // TestContext.CancellationTokenSource.Token の代わりに TestContext.CancellationToken を使用する + CancellationToken CancellationToken => TestContext.CancellationTokenSource.Token; +#pragma warning restore MSTEST0054 // TestContext.CancellationTokenSource.Token の代わりに TestContext.CancellationToken を使用する + static int Run(string[] args) + { + var entryPoint = typeof(Program).Assembly.EntryPoint; + Assert.IsNotNull(entryPoint); + object?[] parameters = [args]; + var result = entryPoint.Invoke(null, parameters) as int?; + Assert.IsNotNull(result); + return result.Value; + } [TestMethod] - public async Task RunAsync_Default_ReturnsZero() + public void Run_Default_ReturnsZero() { - var exitCode = await Program.RunAsync(Array.Empty(), CancellationToken.None); + var exitCode = Run([]); Assert.AreEqual(0, exitCode); } + const string HelloWorldProgram = "64+\"!dlroW ,olleH\">:#,_@"; [TestMethod] - public void EntryPoint_Invoke_ReturnsZero() + public void Run_HelpOption_ReturnsZero() { - var entryPoint = typeof(Program).Assembly.EntryPoint!; - var task = (Task)entryPoint.Invoke(null, new object[] { Array.Empty() })!; - var exitCode = task.Result; + var exitCode = Run(["--help"]); Assert.AreEqual(0, exitCode); } + + [TestMethod] + public async Task Run_HelloWorld_ReturnsZero() + { + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.b98"); + try + { + await File.WriteAllTextAsync(path, HelloWorldProgram, CancellationToken); + + var exitCode = Run([path]); + Assert.AreEqual(0, exitCode); + } + finally + { + if (File.Exists(path)) + File.Delete(path); + } + } + + [TestMethod] + public async Task RunAsync_CancelledToken_StopsInfiniteProgram() + { + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.b98"); + try + { + await File.WriteAllTextAsync(path, ">", CancellationToken); + + using var cancellation = new CancellationTokenSource(); + cancellation.Cancel(); + + var exitCode = await Program.RunAsync([path], cancellationToken: cancellation.Token); + Assert.AreEqual(0, exitCode); + } + finally + { + if (File.Exists(path)) + File.Delete(path); + } + } } From 3823955667bf6bea1f3f2beac8cb7421a4463ced Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 18:34:07 +0900 Subject: [PATCH 10/25] fix(funge-interpreter): restore original test cases with Run helper --- Interpreter.Tests/ProgramTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Interpreter.Tests/ProgramTests.cs b/Interpreter.Tests/ProgramTests.cs index e02d41b..ebbd8da 100644 --- a/Interpreter.Tests/ProgramTests.cs +++ b/Interpreter.Tests/ProgramTests.cs @@ -6,9 +6,10 @@ namespace Esolang.Funge.Interpreter.Tests; [TestClass] public class ProgramTests(TestContext TestContext) { -#pragma warning disable MSTEST0054 // TestContext.CancellationTokenSource.Token の代わりに TestContext.CancellationToken を使用する +#pragma warning disable MSTEST0054 CancellationToken CancellationToken => TestContext.CancellationTokenSource.Token; -#pragma warning restore MSTEST0054 // TestContext.CancellationTokenSource.Token の代わりに TestContext.CancellationToken を使用する +#pragma warning restore MSTEST0054 + static int Run(string[] args) { var entryPoint = typeof(Program).Assembly.EntryPoint; @@ -18,13 +19,13 @@ static int Run(string[] args) Assert.IsNotNull(result); return result.Value; } + [TestMethod] public void Run_Default_ReturnsZero() { var exitCode = Run([]); Assert.AreEqual(0, exitCode); } - const string HelloWorldProgram = "64+\"!dlroW ,olleH\">:#,_@"; [TestMethod] public void Run_HelpOption_ReturnsZero() @@ -39,8 +40,7 @@ public async Task Run_HelloWorld_ReturnsZero() var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.b98"); try { - await File.WriteAllTextAsync(path, HelloWorldProgram, CancellationToken); - + await File.WriteAllTextAsync(path, "64+\"!dlroW ,olleH\">:#,_@", CancellationToken); var exitCode = Run([path]); Assert.AreEqual(0, exitCode); } From 28d0c5acdec35a14f02d42c16860d4755e20fe12 Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 18:39:58 +0900 Subject: [PATCH 11/25] fix(funge-interpreter): restore full test suite and format --- Interpreter/Program.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Interpreter/Program.cs b/Interpreter/Program.cs index a635735..01d103a 100644 --- a/Interpreter/Program.cs +++ b/Interpreter/Program.cs @@ -10,26 +10,18 @@ void OnCancelKeyPress(object? _, ConsoleCancelEventArgs e) Console.CancelKeyPress += OnCancelKeyPress; try { - return await Program.RunAsync(args, cancellation.Token); + return await RunAsync(args, cancellation.Token); } finally { Console.CancelKeyPress -= OnCancelKeyPress; } -namespace Esolang.Funge.Interpreter; - /// /// Entry point for the dotnet-funge command-line tool. /// -public partial class Program +internal partial class Program { - /// - /// Runs the command-line pipeline and returns the process exit code. - /// - /// Command-line arguments. - /// Token to cancel command execution. - /// The exit code. public static async Task RunAsync(string[] args, CancellationToken cancellationToken = default) { var rootCommand = FungeInterpreterExtensions.BuildRootCommand(); From 42ed0a9da36d1a249dc769bcfdd883621ab7a7ba Mon Sep 17 00:00:00 2001 From: juner Date: Tue, 26 May 2026 23:14:45 +0900 Subject: [PATCH 12/25] test: update help option exit code expectation to 0 --- Interpreter.Tests/ProgramTests.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Interpreter.Tests/ProgramTests.cs b/Interpreter.Tests/ProgramTests.cs index ebbd8da..dce4fba 100644 --- a/Interpreter.Tests/ProgramTests.cs +++ b/Interpreter.Tests/ProgramTests.cs @@ -1,6 +1,3 @@ -using Esolang.Funge.Interpreter; -using Microsoft.VisualStudio.TestTools.UnitTesting; - namespace Esolang.Funge.Interpreter.Tests; [TestClass] @@ -21,10 +18,10 @@ static int Run(string[] args) } [TestMethod] - public void Run_Default_ReturnsZero() + public void Run_Default_ReturnsOne() { var exitCode = Run([]); - Assert.AreEqual(0, exitCode); + Assert.AreEqual(1, exitCode); } [TestMethod] From ba6d65f2c7954e4524b349870a96307ad3f800aa Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 28 May 2026 09:41:29 +0900 Subject: [PATCH 13/25] =?UTF-8?q?csproj=20=E3=81=AE=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Generator.Tests/Esolang.Funge.Generator.Tests.csproj | 6 ------ Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj | 5 ----- 2 files changed, 11 deletions(-) diff --git a/Generator.Tests/Esolang.Funge.Generator.Tests.csproj b/Generator.Tests/Esolang.Funge.Generator.Tests.csproj index ab9420a..12b8adb 100644 --- a/Generator.Tests/Esolang.Funge.Generator.Tests.csproj +++ b/Generator.Tests/Esolang.Funge.Generator.Tests.csproj @@ -3,12 +3,6 @@ net48;net8.0;net9.0;net10.0 net8.0;net9.0;net10.0 - enable - enable - - false - true - false Esolang.Funge.Generator.Tests diff --git a/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj b/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj index 955ba42..2de7d45 100644 --- a/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj +++ b/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj @@ -2,11 +2,6 @@ net8.0;net9.0;net10.0 - enable - enable - false - true - false Funge.Interpreter.Tests From 4368c1be660c93c7e6589747657eadfe9af7b5b7 Mon Sep 17 00:00:00 2001 From: juner Date: Sun, 31 May 2026 07:42:28 +0900 Subject: [PATCH 14/25] =?UTF-8?q?Esolang.Abstractions=20=E3=82=92=E6=8C=81?= =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=81=8D=E3=81=A6=E5=AF=BE=E5=BF=9C=E3=81=AE?= =?UTF-8?q?=E6=BA=96=E5=82=99=E3=82=92=E8=A1=8C=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.targets | 3 +- Generator/Esolang.Funge.Generator.csproj | 4 + .../.editorconfig | 2 + .../BindingError.cs | 110 ++++++++ .../IsExternalInit.cs | 11 + .../KnownTypes.cs | 249 ++++++++++++++++++ .../MethodInputKind.cs | 17 ++ .../MethodOutputKind.cs | 21 ++ .../MethodReturnKind.cs | 38 +++ .../MethodSignatureBinder.cs | 228 ++++++++++++++++ .../MethodSignatureBinding.cs | 98 +++++++ .../.editorconfig | 2 + .../IProcessor.cs | 106 ++++++++ .../PipeProcessorExtensions.cs | 112 ++++++++ .../TextProcessorExtensions.cs | 112 ++++++++ 15 files changed, 1112 insertions(+), 1 deletion(-) create mode 100644 Generator/Esolang.Generator.Abstractions/.editorconfig create mode 100644 Generator/Esolang.Generator.Abstractions/BindingError.cs create mode 100644 Generator/Esolang.Generator.Abstractions/IsExternalInit.cs create mode 100644 Generator/Esolang.Generator.Abstractions/KnownTypes.cs create mode 100644 Generator/Esolang.Generator.Abstractions/MethodInputKind.cs create mode 100644 Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs create mode 100644 Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs create mode 100644 Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs create mode 100644 Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs create mode 100644 Processor/Esolang.Processor.Abstractions/.editorconfig create mode 100644 Processor/Esolang.Processor.Abstractions/IProcessor.cs create mode 100644 Processor/Esolang.Processor.Abstractions/PipeProcessorExtensions.cs create mode 100644 Processor/Esolang.Processor.Abstractions/TextProcessorExtensions.cs diff --git a/Directory.Build.targets b/Directory.Build.targets index 222a772..4d63d3d 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -5,7 +5,6 @@ - @@ -14,7 +13,9 @@ + + diff --git a/Generator/Esolang.Funge.Generator.csproj b/Generator/Esolang.Funge.Generator.csproj index 122bd6e..9d64802 100644 --- a/Generator/Esolang.Funge.Generator.csproj +++ b/Generator/Esolang.Funge.Generator.csproj @@ -18,6 +18,10 @@ + + all + runtime; build; native; contentfiles; analyzers + diff --git a/Generator/Esolang.Generator.Abstractions/.editorconfig b/Generator/Esolang.Generator.Abstractions/.editorconfig new file mode 100644 index 0000000..50c37f6 --- /dev/null +++ b/Generator/Esolang.Generator.Abstractions/.editorconfig @@ -0,0 +1,2 @@ +[*] +generated_code = true diff --git a/Generator/Esolang.Generator.Abstractions/BindingError.cs b/Generator/Esolang.Generator.Abstractions/BindingError.cs new file mode 100644 index 0000000..84ab00d --- /dev/null +++ b/Generator/Esolang.Generator.Abstractions/BindingError.cs @@ -0,0 +1,110 @@ +#nullable enable +using Microsoft.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; + +namespace Esolang.Generator; + +/// +/// Specifies the kind of diagnostic error that occurred during method signature binding. +/// +public enum BindingErrorKind +{ + /// The return type of the method is not supported. + UnsupportedReturnType, + /// A parameter has an invalid modifier (e.g., ref, out, in). + InvalidParameterModifier, + /// More than one parameter is competing for the same input role. + DuplicateInput, + /// More than one parameter is competing for the same output role. + DuplicateOutput, + /// More than one cancellation token parameter was found. + DuplicateCancellationToken, + /// More than one logger parameter was found. + DuplicateLogger, + /// A conflict exists between the return type and an output parameter. + ReturnOutputConflict, +} + +/// +/// Represents a diagnostic error that occurred during method signature binding. +/// +[ExcludeFromCodeCoverage] +public abstract record BindingError +{ + /// + /// + /// + /// The kind of error. + /// The location associated with the error. + BindingError(BindingErrorKind Kind, Location? Location) : base() + => (this.Kind, this.Location) = (Kind, Location); + + /// + /// The kind of error. + /// + public BindingErrorKind Kind { get; } + + /// + /// The location associated with the error. + /// + public Location? Location { get; } + + /// + /// The return type of the method is not supported. + /// + /// The unsupported return type symbol. + /// The location of the return type. + public sealed record UnsupportedReturnType(ITypeSymbol ReturnType, Location? Location) + : BindingError(BindingErrorKind.UnsupportedReturnType, Location); + + /// + /// A parameter has an invalid modifier (e.g., ref, out, in). + /// + /// The parameter with the invalid modifier. + /// The location of the parameter. + public sealed record InvalidParameterModifier(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.InvalidParameterModifier, Location); + + /// + /// More than one parameter is competing for the same input role. + /// + /// The parameter that caused the duplication. + /// The kind of input that was already assigned. + /// The location of the duplicate parameter. + public sealed record DuplicateInput(IParameterSymbol Parameter, MethodInputKind ExistingKind, Location? Location) + : BindingError(BindingErrorKind.DuplicateInput, Location); + + /// + /// More than one parameter is competing for the same output role. + /// + /// The parameter that caused the duplication. + /// The kind of output that was already assigned. + /// The location of the duplicate parameter. + public sealed record DuplicateOutput(IParameterSymbol Parameter, MethodOutputKind ExistingKind, Location? Location) + : BindingError(BindingErrorKind.DuplicateOutput, Location); + + /// + /// More than one cancellation token parameter was found. + /// + /// The duplicate cancellation token parameter. + /// The location of the duplicate parameter. + public sealed record DuplicateCancellationToken(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.DuplicateCancellationToken, Location); + + /// + /// More than one logger parameter was found. + /// + /// The duplicate logger parameter. + /// The location of the duplicate parameter. + public sealed record DuplicateLogger(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.DuplicateLogger, Location); + + /// + /// A conflict exists between the return type and an output parameter. + /// + /// The output parameter that conflicts with the return type. + /// The location of the conflicting parameter. + public sealed record ReturnOutputConflict(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.ReturnOutputConflict, Location); + +} diff --git a/Generator/Esolang.Generator.Abstractions/IsExternalInit.cs b/Generator/Esolang.Generator.Abstractions/IsExternalInit.cs new file mode 100644 index 0000000..f76ff9f --- /dev/null +++ b/Generator/Esolang.Generator.Abstractions/IsExternalInit.cs @@ -0,0 +1,11 @@ +namespace System.Runtime.CompilerServices; + +#if !NET5_0_OR_GREATER +/// +/// Reserved to be used by the compiler for tracking metadata. +/// This class should not be used by developers in source code. +/// +static class IsExternalInit +{ +} +#endif diff --git a/Generator/Esolang.Generator.Abstractions/KnownTypes.cs b/Generator/Esolang.Generator.Abstractions/KnownTypes.cs new file mode 100644 index 0000000..add5e5c --- /dev/null +++ b/Generator/Esolang.Generator.Abstractions/KnownTypes.cs @@ -0,0 +1,249 @@ +#nullable enable +using Microsoft.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Esolang.Generator; + +/// +/// Holds resolved type symbols for a compilation. +/// +/// +/// Initializes a new instance of the struct. +/// +/// The compilation to resolve types from. +[ExcludeFromCodeCoverage] +public readonly struct KnownTypes(Compilation compilation) +{ + /// The string type symbol. + public readonly INamedTypeSymbol? String = compilation.GetSpecialType(SpecialType.System_String); + /// The byte type symbol. + public readonly INamedTypeSymbol? Byte = compilation.GetSpecialType(SpecialType.System_Byte); + /// The int type symbol. + public readonly INamedTypeSymbol? Int32 = compilation.GetSpecialType(SpecialType.System_Int32); + /// The System.Threading.Tasks.Task type symbol. + public readonly INamedTypeSymbol? Task = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task"); + /// The System.Threading.Tasks.Task{TResult} type symbol. + public readonly INamedTypeSymbol? TaskT = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1"); + /// The System.Threading.Tasks.ValueTask type symbol. + public readonly INamedTypeSymbol? ValueTask = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask"); + /// The System.Threading.Tasks.ValueTask{TResult} type symbol. + public readonly INamedTypeSymbol? ValueTaskT = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); + /// The System.Collections.Generic.IEnumerable{T} type symbol. + public readonly INamedTypeSymbol? IEnumerableT = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IEnumerable`1"); + /// The System.Collections.Generic.IAsyncEnumerable{T} type symbol. + public readonly INamedTypeSymbol? IAsyncEnumerableT = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1"); + /// The System.IO.Pipelines.PipeReader type symbol. + public readonly INamedTypeSymbol? PipeReader = compilation.GetBestTypeByMetadataName("System.IO.Pipelines.PipeReader"); + /// The System.IO.Pipelines.PipeWriter type symbol. + public readonly INamedTypeSymbol? PipeWriter = compilation.GetBestTypeByMetadataName("System.IO.Pipelines.PipeWriter"); + /// The System.IO.TextReader type symbol. + public readonly INamedTypeSymbol? TextReader = compilation.GetBestTypeByMetadataName("System.IO.TextReader"); + /// The System.IO.TextWriter type symbol. + public readonly INamedTypeSymbol? TextWriter = compilation.GetBestTypeByMetadataName("System.IO.TextWriter"); + /// The System.Threading.CancellationToken type symbol. + public readonly INamedTypeSymbol? CancellationToken = compilation.GetBestTypeByMetadataName("System.Threading.CancellationToken"); + /// The Microsoft.Extensions.Logging.ILogger type symbol. + public readonly INamedTypeSymbol? ILogger = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger"); + /// The Microsoft.Extensions.Logging.ILogger{T} type symbol. + public readonly INamedTypeSymbol? ILoggerT = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger`1"); + + static bool EqualsDefinition(ITypeSymbol? type, ISymbol? symbol) => + type != null && symbol != null && SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, symbol); + + static bool EqualsType(ITypeSymbol? type, ISymbol? symbol) => + type != null && symbol != null && SymbolEqualityComparer.Default.Equals(type, symbol); + + /// Gets a value indicating whether the type is string. + /// The type to check. + /// Optional: Whether to check for nullability. + public readonly bool IsString(ITypeSymbol? type, bool? isNullable = null) + { + if (type is not INamedTypeSymbol named || !SymbolEqualityComparer.Default.Equals(named, String)) return false; + if (isNullable == null) return true; + if (isNullable.Value) return type.NullableAnnotation == NullableAnnotation.Annotated; + return type.NullableAnnotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; + } + + /// Gets a value indicating whether the type is byte. + public readonly bool IsByte(ITypeSymbol? type) => EqualsType(type, Byte); + /// Gets a value indicating whether the type is int. + public readonly bool IsInt32(ITypeSymbol? type) => EqualsType(type, Int32); + + /// Gets a value indicating whether the type is System.Threading.Tasks.Task. + public readonly bool IsTask(ITypeSymbol? type) => EqualsType(type, Task); + /// Gets a value indicating whether the type is System.Threading.Tasks.Task{TResult}. + public readonly bool IsTaskT(ITypeSymbol? type, bool? isNullable = null) + { + if (type is not INamedTypeSymbol named || !EqualsDefinition(named, TaskT)) return false; + if (isNullable == null) return true; + var annotation = named.TypeArguments[0].NullableAnnotation; + return isNullable.Value ? annotation == NullableAnnotation.Annotated : annotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; + } + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask. + public readonly bool IsValueTask(ITypeSymbol? type) => EqualsType(type, ValueTask); + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{TResult}. + public readonly bool IsValueTaskT(ITypeSymbol? type, bool? isNullable = null) + { + if (type is not INamedTypeSymbol named || !EqualsDefinition(named, ValueTaskT)) return false; + if (isNullable == null) return true; + var annotation = named.TypeArguments[0].NullableAnnotation; + return isNullable.Value ? annotation == NullableAnnotation.Annotated : annotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; + } + /// Gets a value indicating whether the type is System.Collections.Generic.IEnumerable{T}. + public readonly bool IsIEnumerableT(ITypeSymbol? type) => EqualsDefinition(type, IEnumerableT); + /// Gets a value indicating whether the type is System.Collections.Generic.IAsyncEnumerable{T}. + public readonly bool IsIAsyncEnumerableT(ITypeSymbol? type) => EqualsDefinition(type, IAsyncEnumerableT); + + /// Gets a value indicating whether the type is System.IO.Pipelines.PipeReader. + public readonly bool IsPipeReader(ITypeSymbol? type) => EqualsType(type, PipeReader); + /// Gets a value indicating whether the type is System.IO.Pipelines.PipeWriter. + public readonly bool IsPipeWriter(ITypeSymbol? type) => EqualsType(type, PipeWriter); + /// Gets a value indicating whether the type is System.IO.TextReader. + public readonly bool IsTextReader(ITypeSymbol? type) => EqualsType(type, TextReader); + /// Gets a value indicating whether the type is System.IO.TextWriter. + public readonly bool IsTextWriter(ITypeSymbol? type) => EqualsType(type, TextWriter); + /// Gets a value indicating whether the type is System.Threading.CancellationToken. + public readonly bool IsCancellationToken(ITypeSymbol? type) => EqualsType(type, CancellationToken); + + /// Gets a value indicating whether the type is System.Threading.Tasks.Task{String}. + public readonly bool IsTaskString(ITypeSymbol? type, bool? isNullable = null) => IsTaskT(type, isNullable) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_String; + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{String}. + public readonly bool IsValueTaskString(ITypeSymbol? type, bool? isNullable = null) => IsValueTaskT(type, isNullable) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_String; + + /// Gets a value indicating whether the type is System.Threading.Tasks.Task{Int32}. + public readonly bool IsTaskInt32(ITypeSymbol? type) => IsTaskT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Int32; + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{Int32}. + public readonly bool IsValueTaskInt32(ITypeSymbol? type) => IsValueTaskT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Int32; + + /// Gets a value indicating whether the type is System.Collections.Generic.IEnumerable{Byte}. + public readonly bool IsIEnumerableByte(ITypeSymbol? type) => IsIEnumerableT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Byte; + /// Gets a value indicating whether the type is System.Collections.Generic.IAsyncEnumerable{Byte}. + public readonly bool IsIAsyncEnumerableByte(ITypeSymbol? type) => IsIAsyncEnumerableT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Byte; + + /// Gets a value indicating whether the type is a logger type (ILogger or ILogger{T}). + public readonly bool IsLogger(ITypeSymbol? type) + { + if (type == null) return false; + if (EqualsType(type, ILogger) || EqualsDefinition(type, ILoggerT)) return true; + foreach (var iface in type.AllInterfaces) + { + if (EqualsType(iface, ILogger) || EqualsDefinition(iface, ILoggerT)) return true; + } + return false; + } + + [ExcludeFromCodeCoverage] + readonly bool PrintMembers(StringBuilder builder) + { + builder.Append(nameof(String)).Append('='); + AppendNamedTypeSymbol(String, builder); + builder.Append(", "); + + builder.Append(nameof(Byte)).Append('='); + AppendNamedTypeSymbol(Byte, builder); + builder.Append(", "); + + builder.Append(nameof(Int32)).Append('='); + AppendNamedTypeSymbol(Int32, builder); + builder.Append(", "); + + builder.Append(nameof(Task)).Append('='); + AppendNamedTypeSymbol(Task, builder); + builder.Append(", "); + + builder.Append(nameof(TaskT)).Append('='); + AppendNamedTypeSymbol(TaskT, builder); + builder.Append(", "); + + builder.Append(nameof(ValueTask)).Append('='); + AppendNamedTypeSymbol(ValueTask, builder); + builder.Append(", "); + + builder.Append(nameof(ValueTaskT)).Append('='); + AppendNamedTypeSymbol(ValueTaskT, builder); + builder.Append(", "); + + builder.Append(nameof(IEnumerableT)).Append('='); + AppendNamedTypeSymbol(IEnumerableT, builder); + builder.Append(", "); + + builder.Append(nameof(IAsyncEnumerableT)).Append('='); + AppendNamedTypeSymbol(IAsyncEnumerableT, builder); + builder.Append(", "); + + builder.Append(nameof(PipeReader)).Append('='); + AppendNamedTypeSymbol(PipeReader, builder); + builder.Append(", "); + + builder.Append(nameof(PipeWriter)).Append('='); + AppendNamedTypeSymbol(PipeWriter, builder); + builder.Append(", "); + + builder.Append(nameof(TextReader)).Append('='); + AppendNamedTypeSymbol(TextReader, builder); + builder.Append(", "); + + builder.Append(nameof(TextWriter)).Append('='); + AppendNamedTypeSymbol(TextWriter, builder); + builder.Append(", "); + + builder.Append(nameof(CancellationToken)).Append('='); + AppendNamedTypeSymbol(CancellationToken, builder); + builder.Append(", "); + + builder.Append(nameof(ILogger)).Append('='); + AppendNamedTypeSymbol(ILogger, builder); + builder.Append(", "); + + builder.Append(nameof(ILoggerT)).Append('='); + AppendNamedTypeSymbol(ILoggerT, builder); + + return true; + static void AppendNamedTypeSymbol(INamedTypeSymbol? symbol, StringBuilder builder) + { + if (symbol == null) return; + builder.Append('('); + builder.Append(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + builder.Append(", "); + builder.Append(nameof(symbol.NullableAnnotation)).Append('=').Append(symbol.NullableAnnotation); + builder.Append(')'); + } + } + + /// + [ExcludeFromCodeCoverage] + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append(nameof(KnownTypes)).Append(" {"); + if (!PrintMembers(builder)) + { + builder.Append(' '); + } + builder.Append('}'); + return builder.ToString(); + } +} + +/// +/// Provides utility methods for resolving types from a . +/// +public static class TypeResolutionExtensions +{ + /// + /// Resolves the best for the specified metadata name. + /// + public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, string metadataName) + { + var type = compilation.GetTypeByMetadataName(metadataName); + if (type != null) return type; + + foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols) + { + var found = assembly.GetTypeByMetadataName(metadataName); + if (found != null) return found; + } + return null; + } +} diff --git a/Generator/Esolang.Generator.Abstractions/MethodInputKind.cs b/Generator/Esolang.Generator.Abstractions/MethodInputKind.cs new file mode 100644 index 0000000..d22a6f7 --- /dev/null +++ b/Generator/Esolang.Generator.Abstractions/MethodInputKind.cs @@ -0,0 +1,17 @@ +#nullable enable +namespace Esolang.Generator; + +/// +/// Specifies the input mechanism of the generated method. +/// +public enum MethodInputKind +{ + /// No explicit input mechanism. + None, + /// Input is provided via a string parameter. + String, + /// Input is provided via a TextReader parameter. + TextReader, + /// Input is provided via a PipeReader parameter. + PipeReader, +} diff --git a/Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs b/Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs new file mode 100644 index 0000000..269af57 --- /dev/null +++ b/Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs @@ -0,0 +1,21 @@ +#nullable enable +namespace Esolang.Generator; + +/// +/// Specifies the output mechanism of the generated method. +/// +public enum MethodOutputKind +{ + /// No explicit output mechanism. + None, + /// Output is written to a TextWriter parameter. + TextWriter, + /// Output is written to a PipeWriter parameter. + PipeWriter, + /// Output is returned as a string. + ReturnString, + /// Output is yielded via IEnumerable<byte>. + ReturnIEnumerable, + /// Output is yielded via IAsyncEnumerable<byte>. + ReturnIAsyncEnumerable, +} diff --git a/Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs b/Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs new file mode 100644 index 0000000..e0c7f13 --- /dev/null +++ b/Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs @@ -0,0 +1,38 @@ +namespace Esolang.Generator; + +/// +/// Specifies the return type of the generated method. +/// +public enum MethodReturnKind +{ + /// The return type is invalid or unsupported. + Invalid, + /// The method returns void. + Void, + /// The method returns int. + Int32, + /// The method returns string. + String, + /// The method returns string (nullable). + NullableString, + /// The method returns Task. + Task, + /// The method returns Task<int>. + TaskInt32, + /// The method returns Task<string>. + TaskString, + /// The method returns Task<string?>. + TaskNullableString, + /// The method returns ValueTask. + ValueTask, + /// The method returns ValueTask<int>. + ValueTaskInt32, + /// The method returns ValueTask<string>. + ValueTaskString, + /// The method returns ValueTask<string?>. + ValueTaskNullableString, + /// The method returns IEnumerable<byte>. + IEnumerableByte, + /// The method returns IAsyncEnumerable<byte>. + IAsyncEnumerableByte, +} diff --git a/Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs b/Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs new file mode 100644 index 0000000..2c2dd8a --- /dev/null +++ b/Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs @@ -0,0 +1,228 @@ +#nullable enable +using Microsoft.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using static Esolang.Generator.BindingError; + +namespace Esolang.Generator; + +/// +/// Provides utility methods for binding method signatures to . +/// +[ExcludeFromCodeCoverage] +public static class MethodSignatureBinder +{ + /// + /// Binds the specified method symbol to a . + /// + /// The method symbol to bind. + /// The known types for the compilation. + /// The result of the binding. + public static MethodSignatureBinding Bind( + IMethodSymbol method, + KnownTypes types) + { + var returnKind = BindReturnKind(method.ReturnType, types); + if (returnKind == MethodReturnKind.Invalid) + { + return new MethodSignatureBinding(returnKind, MethodInputKind.None, MethodOutputKind.None, "", "", null, null, false, method.Parameters, new UnsupportedReturnType(method.ReturnType, method.Locations.FirstOrDefault())); + } + + var outputKind = BindDefaultOutputKind(returnKind); + var inputKind = MethodInputKind.None; + var inputExpr = ""; + var outputExpr = ""; + string? cancellationTokenName = null; + string? loggerExpression = null; + var isLoggerFromParameter = false; + var unhandledParameters = new List(); + + foreach (var p in method.Parameters) + { + if (p.RefKind != RefKind.None) + { + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new InvalidParameterModifier(p, p.Locations.FirstOrDefault())); + } + + if (types.IsString(p.Type, false)) + { + if (inputKind != MethodInputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); + + inputKind = MethodInputKind.String; + inputExpr = p.Name; + continue; + } + + if (types.IsTextReader(p.Type)) + { + if (inputKind != MethodInputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); + + inputKind = MethodInputKind.TextReader; + inputExpr = p.Name; + continue; + } + + if (types.IsPipeReader(p.Type)) + { + if (inputKind != MethodInputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); + + inputKind = MethodInputKind.PipeReader; + inputExpr = p.Name; + continue; + } + + if (types.IsTextWriter(p.Type)) + { + if (IsOutputReturning(returnKind)) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new ReturnOutputConflict(p, p.Locations.FirstOrDefault())); + + if (outputKind != MethodOutputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateOutput(p, outputKind, p.Locations.FirstOrDefault())); + + outputKind = MethodOutputKind.TextWriter; + outputExpr = p.Name; + continue; + } + + if (types.IsPipeWriter(p.Type)) + { + if (IsOutputReturning(returnKind)) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new ReturnOutputConflict(p, p.Locations.FirstOrDefault())); + + if (outputKind != MethodOutputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateOutput(p, outputKind, p.Locations.FirstOrDefault())); + + outputKind = MethodOutputKind.PipeWriter; + outputExpr = p.Name; + continue; + } + + if (types.IsCancellationToken(p.Type)) + { + if (cancellationTokenName != null) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateCancellationToken(p, p.Locations.FirstOrDefault())); + + cancellationTokenName = p.Name; + continue; + } + + if (types.IsLogger(p.Type)) + { + if (loggerExpression != null) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateLogger(p, p.Locations.FirstOrDefault())); + + loggerExpression = p.Name; + isLoggerFromParameter = true; + continue; + } + + unhandledParameters.Add(p); + } + + loggerExpression ??= FindLoggerInContainingType(method.ContainingType, method.IsStatic, types, out isLoggerFromParameter); + + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, unhandledParameters); + } + + /// + /// Binds the return type symbol to a . + /// + public static MethodReturnKind BindReturnKind(ITypeSymbol returnType, KnownTypes types) + { + if (returnType.SpecialType == SpecialType.System_Void) return MethodReturnKind.Void; + if (returnType.SpecialType == SpecialType.System_Int32) return MethodReturnKind.Int32; + if (types.IsString(returnType, false)) return MethodReturnKind.String; + if (types.IsString(returnType, true)) return MethodReturnKind.NullableString; + if (types.IsTask(returnType)) return MethodReturnKind.Task; + if (types.IsTaskInt32(returnType)) return MethodReturnKind.TaskInt32; + if (types.IsTaskString(returnType, false)) return MethodReturnKind.TaskString; + if (types.IsTaskString(returnType, true)) return MethodReturnKind.TaskNullableString; + if (types.IsValueTask(returnType)) return MethodReturnKind.ValueTask; + if (types.IsValueTaskInt32(returnType)) return MethodReturnKind.ValueTaskInt32; + if (types.IsValueTaskString(returnType, false)) return MethodReturnKind.ValueTaskString; + if (types.IsValueTaskString(returnType, true)) return MethodReturnKind.ValueTaskNullableString; + if (types.IsIEnumerableByte(returnType)) return MethodReturnKind.IEnumerableByte; + if (types.IsIAsyncEnumerableByte(returnType)) return MethodReturnKind.IAsyncEnumerableByte; + + return MethodReturnKind.Invalid; + } + + /// + /// Gets the default output kind based on the return kind. + /// + static MethodOutputKind BindDefaultOutputKind(MethodReturnKind returnKind) => returnKind switch + { + MethodReturnKind.String or MethodReturnKind.NullableString or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString => MethodOutputKind.ReturnString, + MethodReturnKind.IEnumerableByte => MethodOutputKind.ReturnIEnumerable, + MethodReturnKind.IAsyncEnumerableByte => MethodOutputKind.ReturnIAsyncEnumerable, + _ => MethodOutputKind.None + }; + + /// + /// Gets a value indicating whether the return kind implies output is returned. + /// + static bool IsOutputReturning(MethodReturnKind returnKind) => returnKind switch + { + MethodReturnKind.String or MethodReturnKind.NullableString or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString or MethodReturnKind.IEnumerableByte or MethodReturnKind.IAsyncEnumerableByte => true, + _ => false + }; + + /// + /// Searches for a logger in the containing type (fields or constructor parameters). + /// + /// The type to search in. + /// Whether the target method is static. + /// The known types for the compilation. + /// Output: Whether the logger was found in a constructor parameter. + /// The expression to access the logger, or null if not found. + static string? FindLoggerInContainingType(ITypeSymbol? type, bool isStatic, KnownTypes types, out bool isFromParameter) + { + isFromParameter = false; + var currentType = type; + var shadowedNames = new HashSet(StringComparer.Ordinal); + var isBaseType = false; + + while (currentType != null) + { + foreach (var field in currentType.GetMembers().OfType()) + { + if (isStatic && !field.IsStatic) continue; + + // If searching in a base type, the field must be accessible (protected or public) + if (isBaseType && field.DeclaredAccessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal or Accessibility.Public or Accessibility.Internal)) + continue; + + if (types.IsLogger(field.Type)) + { + return field.Name; + } + + if (field.CanBeReferencedByName) + { + shadowedNames.Add(field.Name); + } + } + currentType = currentType.BaseType; + isBaseType = true; + } + + if (type is INamedTypeSymbol namedType) + { + foreach (var constructor in namedType.InstanceConstructors) + { + foreach (var parameter in constructor.Parameters) + { + if (types.IsLogger(parameter.Type) && !shadowedNames.Contains(parameter.Name)) + { + isFromParameter = true; + return parameter.Name; + } + } + } + } + + return null; + } +} diff --git a/Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs b/Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs new file mode 100644 index 0000000..da70428 --- /dev/null +++ b/Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs @@ -0,0 +1,98 @@ +#nullable enable +using Microsoft.CodeAnalysis; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Esolang.Generator; + +/// +/// Represents the result of binding a method signature for generation. +/// +/// The return kind of the method. +/// The input kind of the method. +/// The output kind of the method. +/// The expression to access the input (e.g., parameter name). +/// The expression to access the output (e.g., parameter name). +/// The name of the cancellation token parameter, if any. +/// The expression to access the logger (e.g., "loggerParam", "this._logger"). +/// Whether the logger is obtained from a method parameter. +/// Parameters that were not handled by the common binding logic. +/// The diagnostic error if the binding failed. +[DebuggerDisplay("{ToString(),nq}")] +[ExcludeFromCodeCoverage] +public record struct MethodSignatureBinding( + MethodReturnKind ReturnKind, + MethodInputKind InputKind, + MethodOutputKind OutputKind, + string InputExpression, + string OutputExpression, + string? CancellationTokenName, + string? LoggerExpression, + bool IsLoggerFromParameter, + IReadOnlyList UnhandledParameters, + BindingError? Error = null) +{ + /// Whether the binding is successful. + [MemberNotNullWhen(false, nameof(Error))] + public readonly bool IsValid => Error is null; + + /// Gets a value indicating whether the method has an explicit input mechanism. + public readonly bool HasExplicitInput => InputKind != MethodInputKind.None; + + /// Gets a value indicating whether the method has an explicit output mechanism. + public readonly bool HasExplicitOutput => OutputKind != MethodOutputKind.None; + + /// Gets a value indicating whether the method is asynchronous. + public readonly bool IsAsync => ReturnKind switch + { + MethodReturnKind.Task or MethodReturnKind.TaskInt32 or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or + MethodReturnKind.ValueTask or MethodReturnKind.ValueTaskInt32 or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString or + MethodReturnKind.IAsyncEnumerableByte => true, + _ => false + }; + + /// Gets a value indicating whether the method returns an enumerable. + public readonly bool IsEnumerable => ReturnKind == MethodReturnKind.IEnumerableByte; + + /// Gets a value indicating whether the method returns an async enumerable. + public readonly bool IsAsyncEnumerable => ReturnKind == MethodReturnKind.IAsyncEnumerableByte; + + [ExcludeFromCodeCoverage] + readonly bool PrintMembers(StringBuilder builder) + { + builder.Append(nameof(IsValid)).Append('=').Append(IsValid).Append(", "); + builder.Append(nameof(ReturnKind)).Append('=').Append(ReturnKind).Append(", "); + builder.Append(nameof(InputKind)).Append('=').Append(InputKind).Append(", "); + builder.Append(nameof(OutputKind)).Append('=').Append(OutputKind).Append(", "); + builder.Append(nameof(InputExpression)).Append('=').Append(InputExpression).Append(", "); + builder.Append(nameof(OutputExpression)).Append('=').Append(OutputExpression).Append(", "); + builder.Append(nameof(CancellationTokenName)).Append('=').Append(CancellationTokenName).Append(", "); + builder.Append(nameof(LoggerExpression)).Append('=').Append(LoggerExpression).Append(", "); + builder.Append(nameof(IsLoggerFromParameter)).Append('=').Append(IsLoggerFromParameter).Append(", "); + builder.Append(nameof(UnhandledParameters)).Append("=["); + for (var i = 0; i < UnhandledParameters.Count; i++) + { + if (i > 0) builder.Append(", "); + builder.Append(UnhandledParameters[i].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + builder.Append("], "); + builder.Append(nameof(Error)).Append('=').Append(Error); + return true; + } + + /// + [ExcludeFromCodeCoverage] + public override readonly string ToString() + { + var builder = new StringBuilder(); + builder.Append(nameof(MethodSignatureBinding)).Append(" {"); + if (!PrintMembers(builder)) + { + builder.Append(' '); + } + builder.Append('}'); + return builder.ToString(); + } + +} diff --git a/Processor/Esolang.Processor.Abstractions/.editorconfig b/Processor/Esolang.Processor.Abstractions/.editorconfig new file mode 100644 index 0000000..50c37f6 --- /dev/null +++ b/Processor/Esolang.Processor.Abstractions/.editorconfig @@ -0,0 +1,2 @@ +[*] +generated_code = true diff --git a/Processor/Esolang.Processor.Abstractions/IProcessor.cs b/Processor/Esolang.Processor.Abstractions/IProcessor.cs new file mode 100644 index 0000000..b4becc2 --- /dev/null +++ b/Processor/Esolang.Processor.Abstractions/IProcessor.cs @@ -0,0 +1,106 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; + +namespace Esolang.Processor; + +/// +/// Common base interface for all processors. +/// +public interface IProcessor { } + +/// +/// Common base interface for processors that hold a program to be executed. +/// +/// The type of the parsed program. +public interface IProcessor : IProcessor +{ + /// The parsed program. + TProgram Program { get; } +} + +/// +/// Execution model based on a stream of I/O events. +/// +public interface IEventProcessor : IProcessor +{ + /// + /// Executes the processor and returns a stream of I/O events. + /// + /// The cancellation token. + /// An asynchronous stream of I/O events. + IAsyncEnumerable RunAsyncEnumerable( + CancellationToken cancellationToken = default); +} + +/// +/// Represents an I/O event. +/// +public interface IOEvent +{ + +} + +/// +/// Represents an event requesting a character input. +/// +[ExcludeFromCodeCoverage] +public abstract class InputCharEvent : IOEvent +{ + /// + /// Writes the input character to the processor. + /// + /// The input character. + public abstract void Write(char c); +} + +/// +/// Represents an event requesting an integer input. +/// +[ExcludeFromCodeCoverage] +public abstract class InputIntEvent : IOEvent +{ + /// + /// Writes the input integer to the processor. + /// + /// The input integer. + public abstract void Write(int i); +} + +/// +/// Represents an event that outputs a character. +/// +/// The character to output. +[ExcludeFromCodeCoverage] +public sealed class OutputCharEvent(char Output) : IOEvent +{ + /// + /// The character to output. + /// + public char Output { get; } = Output; +} + +/// +/// Represents an event that outputs an integer. +/// +/// The integer to output. +[ExcludeFromCodeCoverage] +public sealed class OutputIntEvent(int Output) : IOEvent +{ + /// + /// The integer to output. + /// + public int Output { get; } = Output; +} + +/// +/// Represents an event indicating the end of execution. +/// +/// The exit code. +[ExcludeFromCodeCoverage] +public sealed class EndEvent(int exitCode) : IOEvent +{ + /// + /// The exit code. + /// + public int ExitCode { get; } = exitCode; +} diff --git a/Processor/Esolang.Processor.Abstractions/PipeProcessorExtensions.cs b/Processor/Esolang.Processor.Abstractions/PipeProcessorExtensions.cs new file mode 100644 index 0000000..2d8f85e --- /dev/null +++ b/Processor/Esolang.Processor.Abstractions/PipeProcessorExtensions.cs @@ -0,0 +1,112 @@ +#nullable enable +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO.Pipelines; +using System.Text; + +namespace Esolang.Processor; + +/// +/// Provides extension methods for running using and . +/// +[ExcludeFromCodeCoverage] +public static class PipeProcessorExtensions +{ + /// + /// Executes the processor until it reaches an . + /// + /// The event processor. + /// The input pipe reader. + /// The output pipe writer. + /// The cancellation token. + /// The exit code. + /// Thrown when input or output is null depending on the event. + public static async ValueTask RunToEndAsync( + this IEventProcessor processor, + PipeReader? input, + PipeWriter? output, + CancellationToken cancellationToken = default) + { + await foreach (var ev in processor.RunAsyncEnumerable(cancellationToken)) + { + switch (ev) + { + case InputCharEvent inputChar: + if (input == null) + throw new ArgumentNullException(nameof(input)); + var result = await input.ReadAtLeastAsync(1, cancellationToken); + var buffer = ArrayPool.Shared.Rent(1); + try + { +#if NETSTANDARD2_1_OR_GREATER + result.Buffer.Slice(0, 1).CopyTo(buffer.AsSpan()); +#else + result.Buffer.Slice(0, 1).ToArray().CopyTo(buffer, 0); +#endif + input.AdvanceTo(result.Buffer.GetPosition(1)); + inputChar.Write((char)buffer[0]); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + break; + case InputIntEvent inputInt: + if (input == null) + throw new ArgumentNullException(nameof(input)); + var result2 = await input.ReadAtLeastAsync(1, cancellationToken); + var buffer2 = ArrayPool.Shared.Rent(1); + try + { +#if NETSTANDARD2_1_OR_GREATER + result2.Buffer.Slice(0, 4).CopyTo(buffer2.AsSpan()); +#else + result2.Buffer.Slice(0, 4).ToArray().CopyTo(buffer2, 0); +#endif + input.AdvanceTo(result2.Buffer.GetPosition(4)); + inputInt.Write(BitConverter.ToInt32(buffer2, 0)); + } + finally + { + ArrayPool.Shared.Return(buffer2); + } + break; + case OutputCharEvent outputChar: + if (output == null) + throw new ArgumentNullException(nameof(output)); + output.Write(Encoding.UTF8.GetBytes([outputChar.Output])); + await output.FlushAsync(cancellationToken); + break; + case OutputIntEvent outputInt: + if (output == null) + throw new ArgumentNullException(nameof(output)); + output.Write(Encoding.UTF8.GetBytes(outputInt.Output.ToString())); + await output.FlushAsync(cancellationToken); + break; + case EndEvent end: + return end.ExitCode; + } + } + return 0; + } + + /// + /// Executes the processor synchronously until it reaches an . + /// + /// The event processor. + /// The input pipe reader. + /// The output pipe writer. + /// The cancellation token. + /// The exit code. + public static int RunToEnd( + this IEventProcessor processor, + PipeReader? input = null, + PipeWriter? output = null, + CancellationToken cancellationToken = default) + { + var result = RunToEndAsync(processor, input, output, cancellationToken); + if (result.IsCompleted) + return result.GetAwaiter().GetResult(); + return result.AsTask().GetAwaiter().GetResult(); + } +} diff --git a/Processor/Esolang.Processor.Abstractions/TextProcessorExtensions.cs b/Processor/Esolang.Processor.Abstractions/TextProcessorExtensions.cs new file mode 100644 index 0000000..d2b6064 --- /dev/null +++ b/Processor/Esolang.Processor.Abstractions/TextProcessorExtensions.cs @@ -0,0 +1,112 @@ +#nullable enable +using System.Buffers; +using System.Diagnostics.CodeAnalysis; + +namespace Esolang.Processor; + +/// +/// Provides extension methods for running using and . +/// +[ExcludeFromCodeCoverage] +public static class TextProcessorExtensions +{ + /// + /// Executes the processor until it reaches an . + /// + /// The event processor. + /// The input text reader. + /// The output text writer. + /// The cancellation token. + /// The exit code. + /// Thrown when input or output is null depending on the event. + public static async ValueTask RunToEndAsync( + this IEventProcessor processor, + TextReader? input = null, + TextWriter? output = null, + CancellationToken cancellationToken = default) + { + await foreach (var ioEvent in processor.RunAsyncEnumerable(cancellationToken)) + { + switch (ioEvent) + { + case InputCharEvent charInput: + if (input is null) + throw new ArgumentNullException(nameof(input)); + { + var buffer = ArrayPool.Shared.Rent(1); + try + { + int read; + do + { +#if NETSTANDARD2_1_OR_GREATER + read = await input.ReadAsync(buffer.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); +#else + read = await input.ReadAsync(buffer, 0, 1).ConfigureAwait(false); +#endif + if (read < 0) continue; + charInput.Write(buffer[0]); + break; + } while (read < 0 && !cancellationToken.IsCancellationRequested); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + break; + case InputIntEvent intInput: + if (input is null) + throw new ArgumentNullException(nameof(input)); + { + var inputString = await input.ReadLineAsync(); + if (int.TryParse(inputString, out var i)) + { + intInput.Write(i); + } + } + break; + case OutputCharEvent charOutput: + if (output is null) + throw new ArgumentNullException(nameof(output)); + { + await output.WriteAsync(charOutput.Output).ConfigureAwait(false); + await output.FlushAsync().ConfigureAwait(false); + } + break; + case OutputIntEvent intOutput: + if (output is null) + throw new ArgumentNullException(nameof(output)); + { + await output.WriteLineAsync(intOutput.Output.ToString()).ConfigureAwait(false); + await output.FlushAsync().ConfigureAwait(false); + } + break; + case EndEvent endEvent: + return endEvent.ExitCode; + } + } + return 0; + } + + /// + /// Executes the processor synchronously until it reaches an . + /// + /// The event processor. + /// The input text reader. + /// The output text writer. + /// The cancellation token. + /// The exit code. + [Obsolete("Use RunToEndAsync instead.")] + public static int RunToEnd( + this IEventProcessor processor, + TextReader? input = null, + TextWriter? output = null, + CancellationToken cancellationToken = default) + { + var result = RunToEndAsync(processor, input, output, cancellationToken); + if (result.IsCompleted) + return result.GetAwaiter().GetResult(); + return result.AsTask().GetAwaiter().GetResult(); + } +} From 0fa59b73883d4dde04df975350e99483e961f72f Mon Sep 17 00:00:00 2001 From: juner Date: Sun, 31 May 2026 08:58:14 +0900 Subject: [PATCH 15/25] feat(generator): integrate Esolang.Generator.Abstractions for method signature binding and diagnostics --- Generator/MethodGenerator.cs | 530 ++++++++--------------------------- 1 file changed, 113 insertions(+), 417 deletions(-) diff --git a/Generator/MethodGenerator.cs b/Generator/MethodGenerator.cs index 6ee0ffc..cfca72d 100644 --- a/Generator/MethodGenerator.cs +++ b/Generator/MethodGenerator.cs @@ -1,8 +1,10 @@ using Esolang.Funge.Parser; +using Esolang.Generator; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Text; +using static Esolang.Generator.BindingError; namespace Esolang.Funge.Generator; @@ -32,129 +34,14 @@ public sealed partial class MethodGenerator : IIncrementalGenerator """; - // ----------------------------------------------------------------------- - // Enumerations for execution signature binding - // ----------------------------------------------------------------------- - - enum ReturnKind + readonly record struct FungeExecutionBinding( + MethodSignatureBinding Binding, + string? ArgsExpression = null, + string? EnvsExpression = null, + BindingError? FungeError = null) { - Void, - Int, - String, - Task, - TaskInt, - TaskString, - ValueTask, - ValueTaskInt, - ValueTaskString, - EnumerableByte, - AsyncEnumerableByte, - Invalid, - } - - enum InputKind { None, String, TextReader, PipeReader } - - enum OutputKind { None, TextWriter, PipeWriter, ReturnString, ReturnEnumerable, ReturnAsyncEnumerable } - - readonly struct KnownTypes - { - public readonly INamedTypeSymbol? String; - public readonly INamedTypeSymbol? Task; - public readonly INamedTypeSymbol? TaskInt; - public readonly INamedTypeSymbol? TaskString; - public readonly INamedTypeSymbol? ValueTask; - public readonly INamedTypeSymbol? ValueTaskInt; - public readonly INamedTypeSymbol? ValueTaskString; - public readonly INamedTypeSymbol? IEnumerableByte; - public readonly INamedTypeSymbol? IAsyncEnumerableByte; - public readonly INamedTypeSymbol? Byte; - public readonly INamedTypeSymbol? Int; - public readonly INamedTypeSymbol? TextReader; - public readonly INamedTypeSymbol? PipeReader; - public readonly INamedTypeSymbol? TextWriter; - public readonly INamedTypeSymbol? PipeWriter; - public readonly INamedTypeSymbol? CancellationToken; - public readonly INamedTypeSymbol? ILogger; - public readonly INamedTypeSymbol? ILoggerOfT; - - public KnownTypes(Compilation compilation) - { - String = compilation.GetSpecialType(SpecialType.System_String); - var byteSymbol = compilation.GetSpecialType(SpecialType.System_Byte); - var intSymbol = compilation.GetSpecialType(SpecialType.System_Int32); - - var taskGeneric = GetBestTypeByMetadataName(compilation, "System.Threading.Tasks.Task`1"); - Task = GetBestTypeByMetadataName(compilation, "System.Threading.Tasks.Task"); - TaskInt = taskGeneric?.Construct(intSymbol); - TaskString = taskGeneric?.Construct(String); - - var valueTaskGeneric = GetBestTypeByMetadataName(compilation, "System.Threading.Tasks.ValueTask`1"); - ValueTask = GetBestTypeByMetadataName(compilation, "System.Threading.Tasks.ValueTask"); - ValueTaskInt = valueTaskGeneric?.Construct(intSymbol); - ValueTaskString = valueTaskGeneric?.Construct(String); - - var enumerableGeneric = GetBestTypeByMetadataName(compilation, "System.Collections.Generic.IEnumerable`1"); - IEnumerableByte = enumerableGeneric?.Construct(byteSymbol); - - var asyncEnumerableGeneric = GetBestTypeByMetadataName(compilation, "System.Collections.Generic.IAsyncEnumerable`1"); - IAsyncEnumerableByte = asyncEnumerableGeneric?.Construct(byteSymbol); - - Byte = byteSymbol; - Int = intSymbol; - TextReader = GetBestTypeByMetadataName(compilation, "System.IO.TextReader"); - PipeReader = GetBestTypeByMetadataName(compilation, "System.IO.Pipelines.PipeReader"); - TextWriter = GetBestTypeByMetadataName(compilation, "System.IO.TextWriter"); - PipeWriter = GetBestTypeByMetadataName(compilation, "System.IO.Pipelines.PipeWriter"); - CancellationToken = GetBestTypeByMetadataName(compilation, "System.Threading.CancellationToken"); - ILogger = GetBestTypeByMetadataName(compilation, "Microsoft.Extensions.Logging.ILogger"); - ILoggerOfT = GetBestTypeByMetadataName(compilation, "Microsoft.Extensions.Logging.ILogger`1"); - } - - private static INamedTypeSymbol? GetBestTypeByMetadataName(Compilation compilation, string metadataName) - { - var type = compilation.GetTypeByMetadataName(metadataName); - if (type != null) return type; - - // Manual search through references if the standard lookup fails due to ambiguity - foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols) - { - var found = assembly.GetTypeByMetadataName(metadataName); - if (found != null) return found; - } - return null; - } - } - - readonly struct ExecutionBinding( - bool isValid, - ReturnKind returnKind, - InputKind inputKind, - OutputKind outputKind, - string inputExpression, - string outputExpression, - string? argsExpression, - string? envsExpression, - string? cancellationTokenName, - string? loggerExpression, - bool isLoggerFromParameter, - string? errorId, - Location? location = null) - { - public bool IsValid { get; } = isValid; - public ReturnKind ReturnKind { get; } = returnKind; - public InputKind InputKind { get; } = inputKind; - public OutputKind OutputKind { get; } = outputKind; - public string InputExpression { get; } = inputExpression; - public string OutputExpression { get; } = outputExpression; - public string? ArgsExpression { get; } = argsExpression; - public string? EnvsExpression { get; } = envsExpression; - public string? CancellationTokenName { get; } = cancellationTokenName; - public string? LoggerExpression { get; } = loggerExpression; - public bool IsLoggerFromParameter { get; } = isLoggerFromParameter; - public string? ErrorId { get; } = errorId; - public Location? Location { get; } = location; - public bool HasExplicitInput => InputKind is not InputKind.None; - public bool HasExplicitOutput => OutputKind is not OutputKind.None; + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, nameof(FungeError))] + public bool IsValid => Binding.IsValid && FungeError is null; } // ----------------------------------------------------------------------- @@ -314,22 +201,30 @@ internal sealed class {{AttributeName}} : Attribute var binding = BindExecutionSignature(symbol, method, types); if (!binding.IsValid) { - if (binding.ErrorId == DiagnosticDescriptors.InvalidReturnType.Id) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidReturnType, - binding.Location ?? method.Identifier.GetLocation(), - symbol.ReturnType.ToDisplayString())); - else if (binding.ErrorId == DiagnosticDescriptors.DuplicateParameter.Id) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.DuplicateParameter, - binding.Location ?? method.Identifier.GetLocation(), - symbol.Name, symbol.Name)); - else if (binding.ErrorId == DiagnosticDescriptors.ReturnOutputConflict.Id) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ReturnOutputConflict, - binding.Location ?? method.Identifier.GetLocation(), - symbol.Name)); - else - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidParameter, - binding.Location ?? method.Identifier.GetLocation(), - symbol.Name)); + var error = binding.FungeError ?? binding.Binding.Error!; + var location = error.Location ?? method.Identifier.GetLocation(); + + var descriptor = error switch + { + UnsupportedReturnType => DiagnosticDescriptors.InvalidReturnType, + DuplicateInput or DuplicateOutput or DuplicateCancellationToken or DuplicateLogger => DiagnosticDescriptors.DuplicateParameter, + ReturnOutputConflict => DiagnosticDescriptors.ReturnOutputConflict, + _ => DiagnosticDescriptors.InvalidParameter, + }; + + var messageArgs = error switch + { + UnsupportedReturnType e => new object[] { e.ReturnType.ToDisplayString() }, + DuplicateInput e => new object[] { e.Parameter.Type.ToDisplayString(), symbol.Name }, + DuplicateOutput e => new object[] { e.Parameter.Type.ToDisplayString(), symbol.Name }, + DuplicateCancellationToken e => new object[] { e.Parameter.Type.ToDisplayString(), symbol.Name }, + DuplicateLogger e => new object[] { e.Parameter.Type.ToDisplayString(), symbol.Name }, + ReturnOutputConflict e => new object[] { symbol.Name }, + InvalidParameterModifier e => new object[] { e.Parameter.Name }, + _ => new object[] { symbol.Name } + }; + + ctx.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs)); continue; } @@ -349,7 +244,7 @@ internal sealed class {{AttributeName}} : Attribute // Strip the ".funge.txt" intermediate suffix that the .targets file appends const string fungeSuffix = ".funge.txt"; var compareFile = normalizedFile.EndsWith(fungeSuffix, StringComparison.OrdinalIgnoreCase) - ? normalizedFile.Substring(0, normalizedFile.Length - fungeSuffix.Length) + ? normalizedFile[..^fungeSuffix.Length] : normalizedFile; if (string.Equals(compareFile, normalizedSource, StringComparison.OrdinalIgnoreCase) || compareFile.EndsWith("/" + normalizedSource, StringComparison.OrdinalIgnoreCase) @@ -377,15 +272,15 @@ internal sealed class {{AttributeName}} : Attribute // Scan for I/O usage var (usesOutput, usesInput) = ScanFungeIo(space); - if (usesOutput && !binding.HasExplicitOutput) + if (usesOutput && !binding.Binding.HasExplicitOutput) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.RequiredOutputInterface, method.Identifier.GetLocation(), symbol.Name)); - if (usesInput && !binding.HasExplicitInput) + if (usesInput && !binding.Binding.HasExplicitInput) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.RequiredInputInterface, method.Identifier.GetLocation(), symbol.Name)); - if (!usesInput && binding.HasExplicitInput) + if (!usesInput && binding.Binding.HasExplicitInput) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UnusedInputInterface, method.Identifier.GetLocation(), symbol.Name)); @@ -393,8 +288,8 @@ internal sealed class {{AttributeName}} : Attribute var emitted = EmitMethod(symbol, method, space, binding, projDir, displayPath); methodSb.AppendLine(emitted); emittedCount++; - runtimeFeatures |= GetRuntimeFacadeFeatures(binding.ReturnKind); - if (binding.LoggerExpression is not null) + runtimeFeatures |= GetRuntimeFacadeFeatures(binding.Binding.ReturnKind); + if (binding.Binding.LoggerExpression is not null) runtimeFeatures |= RuntimeFacadeFeatures.RunWithLogging; } @@ -405,18 +300,18 @@ internal sealed class {{AttributeName}} : Attribute }); } - static RuntimeFacadeFeatures GetRuntimeFacadeFeatures(ReturnKind returnKind) => returnKind switch + static RuntimeFacadeFeatures GetRuntimeFacadeFeatures(MethodReturnKind returnKind) => returnKind switch { - ReturnKind.Void or ReturnKind.Int => RuntimeFacadeFeatures.RunSync, - ReturnKind.String => RuntimeFacadeFeatures.RunString, - ReturnKind.Task => RuntimeFacadeFeatures.RunTask, - ReturnKind.TaskInt => RuntimeFacadeFeatures.RunTaskInt, - ReturnKind.TaskString => RuntimeFacadeFeatures.RunTaskString, - ReturnKind.ValueTask => RuntimeFacadeFeatures.RunValueTask, - ReturnKind.ValueTaskInt => RuntimeFacadeFeatures.RunValueTaskInt, - ReturnKind.ValueTaskString => RuntimeFacadeFeatures.RunValueTaskString, - ReturnKind.EnumerableByte => RuntimeFacadeFeatures.RunEnumerable, - ReturnKind.AsyncEnumerableByte => RuntimeFacadeFeatures.RunAsyncEnumerable, + MethodReturnKind.Void or MethodReturnKind.Int32 => RuntimeFacadeFeatures.RunSync, + MethodReturnKind.String or MethodReturnKind.NullableString => RuntimeFacadeFeatures.RunString, + MethodReturnKind.Task => RuntimeFacadeFeatures.RunTask, + MethodReturnKind.TaskInt32 => RuntimeFacadeFeatures.RunTaskInt, + MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString => RuntimeFacadeFeatures.RunTaskString, + MethodReturnKind.ValueTask => RuntimeFacadeFeatures.RunValueTask, + MethodReturnKind.ValueTaskInt32 => RuntimeFacadeFeatures.RunValueTaskInt, + MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString => RuntimeFacadeFeatures.RunValueTaskString, + MethodReturnKind.IEnumerableByte => RuntimeFacadeFeatures.RunEnumerable, + MethodReturnKind.IAsyncEnumerableByte => RuntimeFacadeFeatures.RunAsyncEnumerable, _ => RuntimeFacadeFeatures.None, }; @@ -424,260 +319,57 @@ internal sealed class {{AttributeName}} : Attribute // Signature binding // ----------------------------------------------------------------------- - static bool IsSameType(ITypeSymbol? type, INamedTypeSymbol? knownType) - { - if (type is null || knownType is null) return false; - return SymbolEqualityComparer.Default.Equals(type, knownType); - } - - static bool IsSameTypeOrConstructedFrom(ITypeSymbol? type, INamedTypeSymbol? knownType) - { - if (type is null || knownType is null) return false; - if (SymbolEqualityComparer.Default.Equals(type, knownType)) return true; - if (type is INamedTypeSymbol namedType && namedType.IsGenericType && SymbolEqualityComparer.Default.Equals(namedType.ConstructedFrom, knownType)) return true; - return false; - } - - static bool IsLoggerType(ITypeSymbol? type, KnownTypes types) + static FungeExecutionBinding BindExecutionSignature(IMethodSymbol method, MethodDeclarationSyntax syntax, KnownTypes types) { - if (type is null) return false; - if (IsSameType(type, types.ILogger) || IsSameTypeOrConstructedFrom(type, types.ILoggerOfT)) return true; - - foreach (var iface in type.AllInterfaces) - { - if (IsSameType(iface, types.ILogger) || IsSameTypeOrConstructedFrom(iface, types.ILoggerOfT)) return true; - } - return false; - } + var binding = MethodSignatureBinder.Bind(method, types); + if (!binding.IsValid) + return new FungeExecutionBinding(binding); - static ExecutionBinding BindExecutionSignature(IMethodSymbol method, MethodDeclarationSyntax syntax, KnownTypes types) - { - var returnType = method.ReturnType; - - var returnKind = ReturnKind.Invalid; - - if (returnType.SpecialType == SpecialType.System_Void) returnKind = ReturnKind.Void; - else if (returnType.SpecialType == SpecialType.System_Int32) returnKind = ReturnKind.Int; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.String)) returnKind = ReturnKind.String; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.Task)) returnKind = ReturnKind.Task; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.TaskInt)) returnKind = ReturnKind.TaskInt; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.TaskString)) returnKind = ReturnKind.TaskString; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.ValueTask)) returnKind = ReturnKind.ValueTask; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.ValueTaskInt)) returnKind = ReturnKind.ValueTaskInt; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.ValueTaskString)) returnKind = ReturnKind.ValueTaskString; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.IEnumerableByte)) returnKind = ReturnKind.EnumerableByte; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.IAsyncEnumerableByte)) returnKind = ReturnKind.AsyncEnumerableByte; - - if (returnKind == ReturnKind.Invalid) - { - // returnKind is invalid, check types for debugging - // Using a diagnostic for debugging as we are in a generator - // ctx.ReportDiagnostic(...); // Need context here, but BindExecutionSignature doesn't have it. - // We'll have to return an error diagnostic later in Initialize. - // For now, let's keep returnKind as invalid to trigger the error. - } - - if (returnKind == ReturnKind.Invalid) - return new(false, returnKind, InputKind.None, OutputKind.None, "", "", null, null, null, null, false, - DiagnosticDescriptors.InvalidReturnType.Id); - - var outputKind = returnKind switch - { - ReturnKind.String or ReturnKind.TaskString or ReturnKind.ValueTaskString - => OutputKind.ReturnString, - ReturnKind.EnumerableByte => OutputKind.ReturnEnumerable, - ReturnKind.AsyncEnumerableByte => OutputKind.ReturnAsyncEnumerable, - _ => OutputKind.None, - }; - - var inputKind = InputKind.None; - var inputExpr = ""; - var outputExpr = ""; string? argsExpr = null; string? envsExpr = null; - string? cancellationTokenName = null; - var hasCancellationToken = false; - - string? loggerExpression = null; - var isLoggerFromParameter = false; - foreach (var p in method.Parameters) + foreach (var p in binding.UnhandledParameters) { - if (p.RefKind is not RefKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, null, null, null, false, - DiagnosticDescriptors.InvalidParameter.Id, p.Locations.FirstOrDefault()); - - var typeName = p.Type.ToDisplayString(); - - if (IsLoggerType(p.Type, types)) + var type = p.Type; + // Funge-specific logic: match string array or IEnumerable for args/envs + var isStringContainer = false; + if (type is IArrayTypeSymbol arrayType && types.IsString(arrayType.ElementType, false)) { - if (loggerExpression is not null) - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, argsExpr, envsExpr, - cancellationTokenName, null, false, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - loggerExpression = p.Name; - isLoggerFromParameter = true; - continue; + isStringContainer = true; } - - if (typeName == "string") + else if (type is INamedTypeSymbol namedType && namedType.IsGenericType) { - if (inputKind is not InputKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, null, null, null, false, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); - inputKind = InputKind.String; - inputExpr = p.Name; - continue; + if (SymbolEqualityComparer.Default.Equals(namedType.ConstructedFrom, types.IEnumerableT) + && types.IsString(namedType.TypeArguments[0], false)) + { + isStringContainer = true; + } } - // String array or IEnumerable - if (p.Type is IArrayTypeSymbol || (p.Type is INamedTypeSymbol namedType && (namedType.Name == "IEnumerable" || namedType.Name == "IEnumerable`1"))) + if (isStringContainer) { - // This is a rough check, keeping existing logic var name = p.Name.ToLowerInvariant(); if (name.Contains("arg")) { if (argsExpr is not null) - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, null, null, null, null, false, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); + return new(binding, FungeError: new DuplicateInput(p, MethodInputKind.None, p.Locations.FirstOrDefault())); argsExpr = p.Name; continue; } if (name.Contains("env")) { if (envsExpr is not null) - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, null, null, null, null, false, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); + return new(binding, FungeError: new DuplicateInput(p, MethodInputKind.None, p.Locations.FirstOrDefault())); envsExpr = p.Name; continue; } } - if (typeName.Contains("TextReader")) - { - if (inputKind is not InputKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, null, null, null, false, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); - inputKind = InputKind.TextReader; - inputExpr = p.Name; - continue; - } - - if (typeName.Contains("PipeReader")) - { - if (inputKind is not InputKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, null, null, null, false, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); - inputKind = InputKind.PipeReader; - inputExpr = p.Name; - continue; - } - - if (typeName.Contains("PipeWriter")) - { - if (outputKind is OutputKind.ReturnString or OutputKind.ReturnEnumerable or OutputKind.ReturnAsyncEnumerable) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.ReturnOutputConflict.Id, - p.Locations.FirstOrDefault()); - if (outputKind is not OutputKind.None) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - outputKind = OutputKind.PipeWriter; - outputExpr = p.Name; - continue; - } - - if (typeName.Contains("TextWriter")) - { - if (outputKind is OutputKind.ReturnString or OutputKind.ReturnEnumerable or OutputKind.ReturnAsyncEnumerable) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.ReturnOutputConflict.Id, - p.Locations.FirstOrDefault()); - if (outputKind is not OutputKind.None) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - outputKind = OutputKind.TextWriter; - outputExpr = p.Name; - continue; - } - - if (typeName.Contains("CancellationToken")) - { - if (hasCancellationToken) - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - hasCancellationToken = true; - cancellationTokenName = p.Name; - continue; - } - - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.InvalidParameter.Id, - p.Locations.FirstOrDefault()); - } - - var fieldName = FindLoggerField(method.ContainingType, method.IsStatic, types, out var isField); - if (loggerExpression == null) - { - loggerExpression = fieldName; - isLoggerFromParameter = !isField; - } - - return new(true, returnKind, inputKind, outputKind, inputExpr, outputExpr, argsExpr, envsExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, null); - } - - static string? FindLoggerField(ITypeSymbol? type, bool isStatic, KnownTypes types, out bool isField) - { - isField = false; - var isBaseType = false; - var shadowedNames = new System.Collections.Generic.HashSet(System.StringComparer.Ordinal); - var currentType = type; - - while (currentType != null) - { - foreach (var field in currentType.GetMembers().OfType()) - { - if (isStatic && !field.IsStatic) continue; - - // If searching in a base type, the field must be accessible (protected or public) - if (isBaseType && field.DeclaredAccessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal or Accessibility.Public or Accessibility.Internal)) - continue; - - if (IsLoggerType(field.Type, types)) - { - isField = true; - return field.Name; - } - else if (field.CanBeReferencedByName) - { - shadowedNames.Add(field.Name); - } - } - currentType = currentType.BaseType; - isBaseType = true; + // Still unhandled + return new(binding, FungeError: new InvalidParameterModifier(p, p.Locations.FirstOrDefault())); } - if (type is INamedTypeSymbol namedType) - { - foreach (var constructor in namedType.InstanceConstructors) - { - if (constructor.DeclaringSyntaxReferences.Any(ds => ds.GetSyntax() is ClassDeclarationSyntax)) - { - foreach (var parameter in constructor.Parameters) - { - if (IsLoggerType(parameter.Type, types) && !shadowedNames.Contains(parameter.Name)) - { - isField = false; - return parameter.Name; - } - } - } - } - } - return null; + return new(binding, argsExpr, envsExpr); } // ----------------------------------------------------------------------- @@ -688,7 +380,7 @@ static string EmitMethod( IMethodSymbol symbol, MethodDeclarationSyntax syntax, FungeSpace space, - ExecutionBinding binding, + FungeExecutionBinding binding, string? projDir, string sourcePath) { @@ -707,14 +399,14 @@ static string EmitMethod( var typeName = symbol.ContainingType.Name; var accessibility = GetAccessibility(symbol.DeclaredAccessibility); var staticMod = symbol.IsStatic ? " static" : string.Empty; - var asyncMod = binding.ReturnKind == ReturnKind.AsyncEnumerableByte ? " async" : string.Empty; + var asyncMod = binding.Binding.ReturnKind == MethodReturnKind.IAsyncEnumerableByte ? " async" : string.Empty; var returnTypeSyntax = symbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var paramList = string.Join(", ", Enumerable.Select(symbol.Parameters, p => + var paramList = string.Join(", ", symbol.Parameters.Select(p => { - var prefix = (binding.ReturnKind == ReturnKind.AsyncEnumerableByte - && binding.CancellationTokenName is not null - && string.Equals(p.Name, binding.CancellationTokenName, StringComparison.Ordinal)) + var prefix = (binding.Binding.IsAsyncEnumerable + && binding.Binding.CancellationTokenName is not null + && string.Equals(p.Name, binding.Binding.CancellationTokenName, StringComparison.Ordinal)) ? "[global::System.Runtime.CompilerServices.EnumeratorCancellation] " : string.Empty; return prefix + p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + " " + p.Name; @@ -740,21 +432,22 @@ static string EmitMethod( return sb.ToString(); } - static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding binding, bool isStatic) + static void EmitBody(StringBuilder sb, FungeSpace space, FungeExecutionBinding fungeBinding, bool isStatic) { + var binding = fungeBinding.Binding; var inputExpr = binding.InputKind switch { - InputKind.None => "global::System.IO.TextReader.Null", - InputKind.String => $"new global::System.IO.StringReader({binding.InputExpression} ?? string.Empty)", - InputKind.TextReader => binding.InputExpression, - InputKind.PipeReader => $"new global::System.IO.StreamReader({binding.InputExpression}.AsStream())", + MethodInputKind.None => "global::System.IO.TextReader.Null", + MethodInputKind.String => $"new global::System.IO.StringReader({binding.InputExpression} ?? string.Empty)", + MethodInputKind.TextReader => binding.InputExpression, + MethodInputKind.PipeReader => $"new global::System.IO.StreamReader({binding.InputExpression}.AsStream())", _ => "global::System.IO.TextReader.Null", }; var cancellationTokenExpr = binding.CancellationTokenName is null ? "global::System.Threading.CancellationToken.None" : binding.CancellationTokenName; - var argsExpr = binding.ArgsExpression ?? "null"; - var envsExpr = binding.EnvsExpression ?? "null"; + var argsExpr = fungeBinding.ArgsExpression ?? "null"; + var envsExpr = fungeBinding.EnvsExpression ?? "null"; EmitSpaceData(sb, space); @@ -767,9 +460,9 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin switch (binding.ReturnKind) { - case ReturnKind.Void: + case MethodReturnKind.Void: { - if (binding.OutputKind == OutputKind.PipeWriter) + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($""" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true); @@ -779,7 +472,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -790,8 +483,8 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin break; } - case ReturnKind.Int: - if (binding.OutputKind == OutputKind.PipeWriter) + case MethodReturnKind.Int32: + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($""" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true); @@ -801,7 +494,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -811,16 +504,17 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } break; - case ReturnKind.String: + case MethodReturnKind.String: + case MethodReturnKind.NullableString: sb.AppendLine($""" return global::Esolang.Funge.__Generated.FungeRuntime.RunString( __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}, {argsExpr}, {envsExpr}{loggerExpr}); """); break; - case ReturnKind.Task: + case MethodReturnKind.Task: { - if (binding.OutputKind == OutputKind.PipeWriter) + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($""" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true); @@ -830,7 +524,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -841,8 +535,8 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin break; } - case ReturnKind.TaskInt: - if (binding.OutputKind == OutputKind.PipeWriter) + case MethodReturnKind.TaskInt32: + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($$""" return __RunTaskIntWithPipeWriter(); @@ -857,7 +551,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -867,16 +561,17 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } break; - case ReturnKind.TaskString: + case MethodReturnKind.TaskString: + case MethodReturnKind.TaskNullableString: sb.AppendLine($""" return global::Esolang.Funge.__Generated.FungeRuntime.RunTaskString( __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}, {argsExpr}, {envsExpr}{loggerExpr}); """); break; - case ReturnKind.ValueTask: + case MethodReturnKind.ValueTask: { - if (binding.OutputKind == OutputKind.PipeWriter) + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($""" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true); @@ -886,7 +581,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -897,8 +592,8 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin break; } - case ReturnKind.ValueTaskInt: - if (binding.OutputKind == OutputKind.PipeWriter) + case MethodReturnKind.ValueTaskInt32: + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($$""" return __RunValueTaskIntWithPipeWriter(); @@ -913,7 +608,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -923,14 +618,15 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } break; - case ReturnKind.ValueTaskString: + case MethodReturnKind.ValueTaskString: + case MethodReturnKind.ValueTaskNullableString: sb.AppendLine($""" return global::Esolang.Funge.__Generated.FungeRuntime.RunValueTaskString( __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}, {argsExpr}, {envsExpr}{loggerExpr}); """); break; - case ReturnKind.EnumerableByte: + case MethodReturnKind.IEnumerableByte: sb.AppendLine($""" foreach (var __b in global::Esolang.Funge.__Generated.FungeRuntime.RunEnumerable( __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}, {argsExpr}, {envsExpr}{loggerExpr})) @@ -938,7 +634,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin """); break; - case ReturnKind.AsyncEnumerableByte: + case MethodReturnKind.IAsyncEnumerableByte: sb.AppendLine($""" await foreach (var __b in global::Esolang.Funge.__Generated.FungeRuntime.RunAsyncEnumerable( __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}, {argsExpr}, {envsExpr}{loggerExpr})) @@ -1022,7 +718,7 @@ static string MakeRelative(string baseDir, string fullPath) var sep = System.IO.Path.DirectorySeparatorChar.ToString(); if (!baseDir.EndsWith(sep)) baseDir += sep; return fullPath.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase) - ? fullPath.Substring(baseDir.Length) + ? fullPath[baseDir.Length..] : fullPath; } } From aecd9a2f5b2b34deadbb5bf0d60119b78b1e76d0 Mon Sep 17 00:00:00 2001 From: juner Date: Mon, 1 Jun 2026 05:52:31 +0900 Subject: [PATCH 16/25] =?UTF-8?q?=E5=AE=9F=E8=A3=85=E9=80=94=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Processor/Esolang.Funge.Processor.csproj | 1 - Processor/FungeProcessor.IEventProcessor.cs | 63 +++++++++++++++ Processor/FungeProcessor.cs | 87 +++++++-------------- 3 files changed, 90 insertions(+), 61 deletions(-) create mode 100644 Processor/FungeProcessor.IEventProcessor.cs diff --git a/Processor/Esolang.Funge.Processor.csproj b/Processor/Esolang.Funge.Processor.csproj index caca1ea..1f45a68 100644 --- a/Processor/Esolang.Funge.Processor.csproj +++ b/Processor/Esolang.Funge.Processor.csproj @@ -22,7 +22,6 @@ - diff --git a/Processor/FungeProcessor.IEventProcessor.cs b/Processor/FungeProcessor.IEventProcessor.cs new file mode 100644 index 0000000..9cb6110 --- /dev/null +++ b/Processor/FungeProcessor.IEventProcessor.cs @@ -0,0 +1,63 @@ +using Esolang.Processor; +using System.Runtime.CompilerServices; + +namespace Esolang.Funge.Processor; + +public sealed partial class FungeProcessor : IEventProcessor +{ + private sealed class FungeInputCharEvent : InputCharEvent + { + public int? Value { get; private set; } + public override void Write(char c) => Value = c; + } + + private sealed class FungeInputIntEvent : InputIntEvent + { + public int? Value { get; private set; } + public override void Write(int i) => Value = i; + } + + private sealed class FungeState + { + public int ExitCode; + public bool Quit; + public bool SuppressAdvance; + } + + /// + public async IAsyncEnumerable RunAsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var ips = new LinkedList(); + ips.AddFirst(new InstructionPointer(_nextIpId++)); + var state = new FungeState(); + + while (ips.Count > 0 && !state.Quit && !cancellationToken.IsCancellationRequested) + { + var node = ips.First; + while (node is not null && !state.Quit && !cancellationToken.IsCancellationRequested) + { + var nextNode = node.Next; + var ip = node.Value; + + state.SuppressAdvance = false; + foreach (var ev in ExecuteInstruction(ip, ips, node, state)) + { + yield return ev; + } + + if (ip.IsStopped || state.Quit) + { + ips.Remove(node); + } + else if (!state.SuppressAdvance) + { + ip.Position = _space.Advance(ip.Position, ip.Delta); + } + + node = nextNode; + } + } + + yield return new EndEvent(state.ExitCode); + } +} diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index 3b5d8a7..3459962 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -25,7 +25,7 @@ public sealed partial class FungeProcessor( TextWriter? output = null, TextReader? input = null, IEnumerable? commandLineArguments = null, - IEnumerable? environmentVariables = null) : ITextProcessor + IEnumerable? environmentVariables = null) : IProcessor { private readonly FungeSpace _space = space; private readonly TextWriter _output = output ?? Console.Out; @@ -55,55 +55,17 @@ public int Run(CancellationToken cancellationToken = default) /// public int RunToEnd(TextReader? input = null, TextWriter? output = null, CancellationToken cancellationToken = default) - { - var resolvedInput = input ?? _input; - var resolvedOutput = output ?? _output; - - var ips = new LinkedList(); - ips.AddFirst(new InstructionPointer(_nextIpId++)); - var exitCode = 0; - var quit = false; - - while (ips.Count > 0 && !quit && !cancellationToken.IsCancellationRequested) - { - var node = ips.First!; - while (node is not null && !quit && !cancellationToken.IsCancellationRequested) - { - var nextNode = node.Next; - var ip = node.Value; - - var suppressAdvance = false; - ExecuteInstruction(ip, ips, node, ref exitCode, ref quit, ref suppressAdvance, resolvedInput, resolvedOutput); - - if (ip.IsStopped || quit) - { - ips.Remove(node); - } - else if (!suppressAdvance) - { - ip.Position = _space.Advance(ip.Position, ip.Delta); - } - - node = nextNode; - } - } - - return exitCode; - } + => TextProcessorExtensions.RunToEndAsync(this, input ?? _input, output ?? _output, cancellationToken).AsTask().GetAwaiter().GetResult(); /// public ValueTask RunToEndAsync(TextReader? input = null, TextWriter? output = null, CancellationToken cancellationToken = default) - => ValueTask.FromResult(RunToEnd(input, output, cancellationToken)); + => TextProcessorExtensions.RunToEndAsync(this, input ?? _input, output ?? _output, cancellationToken); - private void ExecuteInstruction( + private IEnumerable ExecuteInstruction( InstructionPointer ip, LinkedList ips, LinkedListNode ipNode, - ref int exitCode, - ref bool quit, - ref bool suppressAdvance, - TextReader input, - TextWriter output, + FungeState state, int? overrideCell = null) { var cell = overrideCell ?? _space[ip.Position]; @@ -138,7 +100,7 @@ private void ExecuteInstruction( { ip.StackStack.Push(cell); } - return; + yield break; } switch (cell) @@ -313,7 +275,7 @@ private void ExecuteInstruction( var dir = s >= 0 ? ip.Delta : ip.Delta.Reflect(); for (var i = 0; i < Math.Abs(s); i++) ip.Position = _space.Advance(ip.Position, dir); - suppressAdvance = true; + state.SuppressAdvance = true; break; } @@ -360,27 +322,29 @@ private void ExecuteInstruction( // ── I/O ────────────────────────────────────────────────────────── case '.': // Output Integer - output.Write(ip.StackStack.Pop()); - output.Write(' '); + yield return new OutputIntEvent(ip.StackStack.Pop()); + yield return new OutputCharEvent(' '); break; case ',': // Output Character - output.Write((char)ip.StackStack.Pop()); + yield return new OutputCharEvent((char)ip.StackStack.Pop()); break; case '&': // Input Integer { - var line = input.ReadLine(); - if (line is null) { ip.Delta = ip.Delta.Reflect(); break; } - ip.StackStack.Push(int.TryParse(line.Trim(), out var v) ? v : 0); + var ev = new FungeInputIntEvent(); + yield return ev; + if (ev.Value.HasValue) ip.StackStack.Push(ev.Value.Value); + else ip.Delta = ip.Delta.Reflect(); break; } case '~': // Input Character { - var ch = input.Read(); - if (ch < 0) ip.Delta = ip.Delta.Reflect(); - else ip.StackStack.Push(ch); + var ev = new FungeInputCharEvent(); + yield return ev; + if (ev.Value.HasValue) ip.StackStack.Push(ev.Value.Value); + else ip.Delta = ip.Delta.Reflect(); break; } @@ -431,8 +395,8 @@ private void ExecuteInstruction( break; case 'q': // Quit program immediately - exitCode = ip.StackStack.Pop(); - quit = true; + state.ExitCode = ip.StackStack.Pop(); + state.Quit = true; break; case 'k': // Iterate: execute next instruction n times @@ -467,7 +431,7 @@ private void ExecuteInstruction( { // n=0: skip the operand. IP moves to the position AFTER the operand. ip.Position = _space.Advance(instrPos, ip.Delta); - suppressAdvance = true; + state.SuppressAdvance = true; } else { @@ -476,10 +440,13 @@ private void ExecuteInstruction( // After k finishes, normal advancement continues from the IP's current position, // so position-changing operands such as [ and # behave "from k". var operand = _space[instrPos]; - for (var i = 0; i < n && !ip.IsStopped && !quit; i++) + for (var i = 0; i < n && !ip.IsStopped && !state.Quit; i++) { - var dummy = false; - ExecuteInstruction(ip, ips, ipNode, ref exitCode, ref quit, ref dummy, input, output, operand); + var subState = new FungeState { ExitCode = state.ExitCode, Quit = state.Quit, SuppressAdvance = false }; + foreach (var ev in ExecuteInstruction(ip, ips, ipNode, subState, operand)) + yield return ev; + state.ExitCode = subState.ExitCode; + state.Quit = subState.Quit; } } break; From 967f19b9b495a258654f23929dddf829937ac455 Mon Sep 17 00:00:00 2001 From: juner Date: Mon, 1 Jun 2026 12:22:14 +0900 Subject: [PATCH 17/25] refactor(processor): migrate FungeProcessor to event-based I/O - Removed TextReader/TextWriter from FungeProcessor constructor. - Implemented IProcessor and RunAsyncEnumerable. - Updated Interpreter and Tests to use the event stream for I/O. - Cleaned up accessibility modifiers and using directives across the project. - Modernized code using collection expressions. --- .editorconfig | 82 ++++++++++- Directory.Build.props | 4 +- Generator.Tests/FungeMethodGeneratorTests.cs | 30 ++-- .../PartialMethodConstraintTests.cs | 1 - Generator/MethodGenerator.cs | 20 +-- Interpreter/FungeInterpreterExtensions.cs | 36 ++++- Interpreter/Program.cs | 2 +- Parser.Tests/FungeParserTests.cs | 2 - Parser/FungeSpace.cs | 20 +-- Parser/Shared/HashCode.cs | 2 +- Parser/Shared/IsExternalInit.cs | 2 +- Processor.Tests/FungeProcessorTests.cs | 65 ++++++--- Processor.Tests/InstructionPointerTests.cs | 1 - Processor.Tests/StackStackTests.cs | 2 - Processor/FungeProcessor.IEventProcessor.cs | 131 +++++++++--------- Processor/FungeProcessor.IProcessor.cs | 13 ++ Processor/FungeProcessor.cs | 58 +++----- Processor/InstructionPointer.cs | 2 +- Processor/README.md | 10 +- Processor/StackStack.cs | 4 +- .../Esolang.Funge.Generator.UseConsole.cs | 2 +- 21 files changed, 303 insertions(+), 186 deletions(-) create mode 100644 Processor/FungeProcessor.IProcessor.cs diff --git a/.editorconfig b/.editorconfig index 60ca27f..5348c94 100644 --- a/.editorconfig +++ b/.editorconfig @@ -39,7 +39,7 @@ dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning # Accessibility modifier settings -dotnet_style_require_accessibility_modifiers = for_non_interface_members +dotnet_style_require_accessibility_modifiers = omit_if_default # Expression-level preferences dotnet_style_coalesce_expression = true:warning @@ -236,7 +236,81 @@ dotnet_style_qualification_for_field = false:silent dotnet_style_qualification_for_property = false:warning dotnet_style_qualification_for_method = false:warning dotnet_style_qualification_for_event = false:warning -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning +# 関係演算子の優先順位が明確な場合は、かっこを使用しない +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:warning +# 関係演算子の優先順位が明確な場合は、かっこを使用しない +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:warning +# 関係演算子の優先順位が明確な場合は、かっこを使用しない +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:warning +# 算術演算子の優先順位が明確な場合はかっこを使用しない dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning +# IEnumerable list = new List() { 1, 2 };など、型が緩やかに一致する場合でもコレクション式を使用することを好みます。 対象となる型は、右側の型と一致するか、IEnumerable、ICollection、IList、IReadOnlyCollection、IReadOnlyListのいずれかの型である必要があります。 +dotnet_style_prefer_collection_expression = when_types_loosely_match:warning +# 不要な using ディレクティブを削除する (IDE0005) +dotnet_diagnostic.IDE0005.severity = warning +# is 式と型キャストの代わりにパターン マッチングを使用します。 +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +# 既定の修飾子である場合を除き、アクセシビリティ修飾子を優先します。 +dotnet_style_require_accessibility_modifiers = omit_if_default:warning +# 未使用のプライベート メンバーを削除する (IDE0051) +dotnet_diagnostic.IDE0051.severity = warning + +#region +# CA2012: ValueTask を正しく使用する必要があります +dotnet_diagnostic.CA2012.severity = warning +# CA2016: CancellationToken パラメーターを受け取るメソッドに渡す +dotnet_diagnostic.CA2016.severity = warning +# CA2200: スタックの詳細を保持するために再スローします。 +dotnet_diagnostic.CA2200.severity = warning +# CA2217: 列挙型に FlagsAttributeを付与しないでください。 +dotnet_diagnostic.CA2217.severity = warning +#endregion + +#region MSTest Analyzers + +# MSTEST0017: アサーション引数は正しい順序で渡す必要があります +dotnet_diagnostic.MSTEST0017.severity = warning +# MSTEST0020: TestInitialize メソッドよりもコンストラクターを優先する +dotnet_diagnostic.MSTEST0020.severity = warning +# MSTEST0021: TestCleanup メソッドよりも Dispose を優先する +dotnet_diagnostic.MSTEST0021.severity = warning +# MSTEST0022: Dispose メソッドよりも TestCleanup を優先する +dotnet_diagnostic.MSTEST0022.severity = warning +# MSTEST0023: ブール アサーションを否定しないこと +dotnet_diagnostic.MSTEST0023.severity = warning +# MSTEST0024: TestContext を静的メンバーに格納しない +dotnet_diagnostic.MSTEST0024.severity = warning +# MSTEST0025: 常に失敗するアサートではなく 'Assert.Fail' を使う +dotnet_diagnostic.MSTEST0025.severity = warning +# MSTEST0026: アサーションでの条件付きアクセスを回避する +dotnet_diagnostic.MSTEST0026.severity = warning +# MSTEST0029: パブリック メソッドをテスト メソッドにする必要がある +dotnet_diagnostic.MSTEST0029.severity = warning +# MSTEST0032: 条件が常に真であることがわかっているため、アサーションを確認するか削除してください +dotnet_diagnostic.MSTEST0032.severity = warning +# MSTEST0036: テスト クラス内でシャドウイングを使用しない +dotnet_diagnostic.MSTEST0036.severity = warning +# MSTEST0037: 適切な 'Assert' メソッドを使用する +dotnet_diagnostic.MSTEST0037.severity = warning +# MSTEST0038: 値型では 'Assert.AreSame' または 'Assert.AreNotSame' を使用しない +dotnet_diagnostic.MSTEST0038.severity = warning +# MSTEST0040: 'async void' コンテキスト内でアサートしない +dotnet_diagnostic.MSTEST0040.severity = warning +# MSTEST0044: DataTestMethod よりも TestMethod を優先する +dotnet_diagnostic.MSTEST0044.severity = warning +# MSTEST0045: タイムアウトに協調キャンセルを使用する +dotnet_diagnostic.MSTEST0045.severity = warning +# MSTEST0046: StringAssertではなくAssertを使います +dotnet_diagnostic.MSTEST0046.severity = warning +# MSTEST0049: Flow TestContext キャンセル トークンを使用する +dotnet_diagnostic.MSTEST0049.severity = warning +# MSTEST0051: Assert.Throws には 1 つのステートメントのみを含める必要があります +dotnet_diagnostic.MSTEST0051.severity = warning +# MSTEST0054: TestContext.CancellationToken からのキャンセル トークンを使用する +dotnet_diagnostic.MSTEST0054.severity = warning +# MSTEST0058: catch ブロック内の assert を避ける +dotnet_diagnostic.MSTEST0058.severity = warning +# MSTEST0061: ランタイム チェックの代わりに OSCondition 属性を使用する +dotnet_diagnostic.MSTEST0061.severity = warning + +#endregion \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index cf6dcc5..f7d43d3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,6 +16,7 @@ $(NoWarn);NETSDK1213;CS9057 snupkg True + true @@ -41,8 +42,9 @@ false true false - $(NoWarn);RS1035 + $(NoWarn);RS1035;CS1591 Exe true + true diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index 5704b1d..5df554e 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -10,7 +10,7 @@ namespace Esolang.Funge.Generator.Tests; [TestClass] -public class FungeMethodGeneratorTests(TestContext TestContext) +public class FungeMethodGeneratorTests { void LogWriteLine(string message) => TestContext.WriteLine(message); @@ -18,11 +18,13 @@ public class FungeMethodGeneratorTests(TestContext TestContext) CancellationToken CancellationToken => TestContext.CancellationTokenSource.Token; #pragma warning restore MSTEST0054 // TestContext.CancellationTokenSource.Token の代わりに TestContext.CancellationToken を使用する - Compilation baseCompilation = default!; + readonly Compilation baseCompilation = default!; - [TestInitialize] - public void InitializeCompilation() + readonly TestContext TestContext; + + public FungeMethodGeneratorTests(TestContext TestContext) { + this.TestContext = TestContext; IEnumerable references = #if NET10_0_OR_GREATER Net100.References.All; @@ -199,8 +201,8 @@ await Task.Factory.StartNew(() => var logs = (List)logger.Logs; // Check if we have logs for '1' and '@' - Assert.IsTrue(logs.Any(l => l.Contains("'1'"))); - Assert.IsTrue(logs.Any(l => l.Contains("'@'"))); + Assert.Contains(l => l.Contains("'1'"), logs); + Assert.Contains(l => l.Contains("'@'"), logs); }, CancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } catch (Exception e) when (e is AssertFailedException or TargetInvocationException) @@ -259,8 +261,8 @@ await Task.Factory.StartNew(() => var logs = (List)loggerType.GetField("Logs")!.GetValue(loggerInstance)!; - Assert.IsTrue(logs.Any(l => l.Contains("'1'"))); - Assert.IsTrue(logs.Any(l => l.Contains("'@'"))); + Assert.Contains(l => l.Contains("'1'"), logs); + Assert.Contains(l => l.Contains("'@'"), logs); }, CancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } catch (Exception e) when (e is AssertFailedException or TargetInvocationException) @@ -456,7 +458,7 @@ partial class TestClass var expectedFiles = new[] { "input.cs", "GenerateFungeMethodAttribute.cs", "GenerateFungeMethod.g.cs" }; foreach (var expected in expectedFiles) { - Assert.IsTrue(actualPaths.Any(p => p.Contains(expected, StringComparison.OrdinalIgnoreCase)), $"Missing file: {expected}"); + Assert.Contains(p => p.Contains(expected, StringComparison.OrdinalIgnoreCase), actualPaths, $"Missing file: {expected}"); } } catch (Exception e) when (e is AssertFailedException or TargetInvocationException) @@ -1549,7 +1551,7 @@ public async Task Runtime_FileInput_LoadsIntoSpace() Directory.SetCurrentDirectory(tempDir); File.WriteAllText("input.txt", "A"); - var reversed = new string("input.txt".Reverse().ToArray()); + var reversed = new string([.. "input.txt".Reverse()]); var program = $"00000\"{reversed}\"in000gq"; var source = """ @@ -1602,7 +1604,7 @@ public async Task Runtime_FileOutput_WritesRegion() { Directory.SetCurrentDirectory(tempDir); - var reversed = new string("output.txt".Reverse().ToArray()); + var reversed = new string([.. "output.txt".Reverse()]); var program = $"88*1+000p00000000\"{reversed}\"o@"; var source = """ @@ -1650,7 +1652,7 @@ await Task.Factory.StartNew(() => public async Task Runtime_SystemExec_ReturnsExitCode() { const string command = "exit 7"; - var reversed = new string(command.Reverse().ToArray()); + var reversed = new string([.. command.Reverse()]); var program = $"0\"{reversed}\"=q"; var source = """ @@ -1690,7 +1692,7 @@ await Task.Factory.StartNew(() => public async Task Runtime_SystemExec_FailureIsNonZero() { const string command = "this_command_should_not_exist_12345"; - var reversed = new string(command.Reverse().ToArray()); + var reversed = new string([.. command.Reverse()]); var program = $"0\"{reversed}\"=q"; var source = """ @@ -2331,7 +2333,7 @@ partial class TestClass foreach (var tree in comp.SyntaxTrees) { LogWriteLine($"\n--- {tree.FilePath} ---"); - LogWriteLine(tree.GetText().ToString()); + LogWriteLine(tree.GetText(TestContext.CancellationToken).ToString()); } } catch (Exception e) when (e is AssertFailedException or TargetInvocationException) diff --git a/Generator.Tests/PartialMethodConstraintTests.cs b/Generator.Tests/PartialMethodConstraintTests.cs index 6a57487..3899c58 100644 --- a/Generator.Tests/PartialMethodConstraintTests.cs +++ b/Generator.Tests/PartialMethodConstraintTests.cs @@ -1,6 +1,5 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Esolang.Funge.Generator.Tests; diff --git a/Generator/MethodGenerator.cs b/Generator/MethodGenerator.cs index cfca72d..e129fea 100644 --- a/Generator/MethodGenerator.cs +++ b/Generator/MethodGenerator.cs @@ -215,13 +215,13 @@ internal sealed class {{AttributeName}} : Attribute var messageArgs = error switch { UnsupportedReturnType e => new object[] { e.ReturnType.ToDisplayString() }, - DuplicateInput e => new object[] { e.Parameter.Type.ToDisplayString(), symbol.Name }, - DuplicateOutput e => new object[] { e.Parameter.Type.ToDisplayString(), symbol.Name }, - DuplicateCancellationToken e => new object[] { e.Parameter.Type.ToDisplayString(), symbol.Name }, - DuplicateLogger e => new object[] { e.Parameter.Type.ToDisplayString(), symbol.Name }, - ReturnOutputConflict e => new object[] { symbol.Name }, - InvalidParameterModifier e => new object[] { e.Parameter.Name }, - _ => new object[] { symbol.Name } + DuplicateInput e => [e.Parameter.Type.ToDisplayString(), symbol.Name], + DuplicateOutput e => [e.Parameter.Type.ToDisplayString(), symbol.Name], + DuplicateCancellationToken e => [e.Parameter.Type.ToDisplayString(), symbol.Name], + DuplicateLogger e => [e.Parameter.Type.ToDisplayString(), symbol.Name], + ReturnOutputConflict e => [symbol.Name], + InvalidParameterModifier e => [e.Parameter.Name], + _ => [symbol.Name] }; ctx.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs)); @@ -644,12 +644,6 @@ static void EmitBody(StringBuilder sb, FungeSpace space, FungeExecutionBinding f } } - static void EmitRuntimeRunCall(StringBuilder sb, string inputExpr, string outputExpr, bool hasInput, bool hasOutput) - => sb.AppendLine($""" - global::Esolang.Funge.__Generated.FungeRuntime.Run( - __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {outputExpr}, {(hasInput ? "true" : "false")}, {(hasOutput ? "true" : "false")}); - """); - static void EmitSpaceData(StringBuilder sb, FungeSpace space) { sb.AppendLine($""" diff --git a/Interpreter/FungeInterpreterExtensions.cs b/Interpreter/FungeInterpreterExtensions.cs index 830eea3..10d4fb1 100644 --- a/Interpreter/FungeInterpreterExtensions.cs +++ b/Interpreter/FungeInterpreterExtensions.cs @@ -1,5 +1,6 @@ using Esolang.Funge.Parser; using Esolang.Funge.Processor; +using Esolang.Processor; using System.Collections; using System.CommandLine; @@ -25,7 +26,7 @@ public static RootCommand BuildRootCommand() pathArgument, }; - rootCommand.SetAction((parseResult, cancellationToken) => + rootCommand.SetAction(async (parseResult, cancellationToken) => { var path = parseResult.GetValue(pathArgument)!; var space = FungeParser.ParseFile(path); @@ -34,11 +35,38 @@ public static RootCommand BuildRootCommand() .Select(static entry => $"{entry.Key}={entry.Value}"); var proc = new FungeProcessor( space, - Console.Out, - Console.In, commandLineArguments: [path], environmentVariables: env); - return Task.FromResult(proc.RunToEnd(cancellationToken: cancellationToken)); + + var exitCode = 0; + await foreach (var ioEvent in proc.RunAsyncEnumerable(cancellationToken)) + { + switch (ioEvent) + { + case InputCharEvent ice: + { + var c = Console.In.Read(); + if (c != -1) ice.Write((char)c); + } + break; + case InputIntEvent iie: + { + var line = await Console.In.ReadLineAsync(cancellationToken).ConfigureAwait(false); + if (int.TryParse(line, out var val)) iie.Write(val); + } + break; + case OutputCharEvent oce: + Console.Out.Write(oce.Output); + break; + case OutputIntEvent oie: + Console.Out.Write(oie.Output); + break; + case EndEvent ee: + exitCode = ee.ExitCode; + break; + } + } + return exitCode; }); return rootCommand; diff --git a/Interpreter/Program.cs b/Interpreter/Program.cs index 01d103a..c7c37cd 100644 --- a/Interpreter/Program.cs +++ b/Interpreter/Program.cs @@ -20,7 +20,7 @@ void OnCancelKeyPress(object? _, ConsoleCancelEventArgs e) /// /// Entry point for the dotnet-funge command-line tool. /// -internal partial class Program +partial class Program { public static async Task RunAsync(string[] args, CancellationToken cancellationToken = default) { diff --git a/Parser.Tests/FungeParserTests.cs b/Parser.Tests/FungeParserTests.cs index 5e27f98..bfb5151 100644 --- a/Parser.Tests/FungeParserTests.cs +++ b/Parser.Tests/FungeParserTests.cs @@ -1,5 +1,3 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - namespace Esolang.Funge.Parser.Tests; [TestClass] diff --git a/Parser/FungeSpace.cs b/Parser/FungeSpace.cs index 11024a3..826527a 100644 --- a/Parser/FungeSpace.cs +++ b/Parser/FungeSpace.cs @@ -6,11 +6,11 @@ namespace Esolang.Funge.Parser; /// public sealed class FungeSpace { - private readonly Dictionary _cells = new(); - private int _minX, _minY, _minZ, _maxX, _maxY, _maxZ; - private bool _hasAny; + readonly Dictionary _cells = []; + int _minX, _minY, _minZ, _maxX, _maxY, _maxZ; + bool _hasAny; - private void IncludeInBounds(FungeVector pos) + void IncludeInBounds(FungeVector pos) { if (!_hasAny) { @@ -96,19 +96,19 @@ public FungeVector Advance(FungeVector pos, FungeVector delta) var depth = _maxZ - _minZ + 1; if (nextX < _minX) - nextX = _maxX - ((_minX - nextX - 1) % width); + nextX = _maxX - (_minX - nextX - 1) % width; else if (nextX > _maxX) - nextX = _minX + ((nextX - _maxX - 1) % width); + nextX = _minX + (nextX - _maxX - 1) % width; if (nextY < _minY) - nextY = _maxY - ((_minY - nextY - 1) % height); + nextY = _maxY - (_minY - nextY - 1) % height; else if (nextY > _maxY) - nextY = _minY + ((nextY - _maxY - 1) % height); + nextY = _minY + (nextY - _maxY - 1) % height; if (nextZ < _minZ) - nextZ = _maxZ - ((_minZ - nextZ - 1) % depth); + nextZ = _maxZ - (_minZ - nextZ - 1) % depth; else if (nextZ > _maxZ) - nextZ = _minZ + ((nextZ - _maxZ - 1) % depth); + nextZ = _minZ + (nextZ - _maxZ - 1) % depth; return new FungeVector(nextX, nextY, nextZ); } diff --git a/Parser/Shared/HashCode.cs b/Parser/Shared/HashCode.cs index 96fdf12..54f2cb5 100644 --- a/Parser/Shared/HashCode.cs +++ b/Parser/Shared/HashCode.cs @@ -2,7 +2,7 @@ // Minimal HashCode polyfill for netstandard2.0 namespace System; -internal static class HashCode +static class HashCode { public static int Combine(T1 v1, T2 v2) { diff --git a/Parser/Shared/IsExternalInit.cs b/Parser/Shared/IsExternalInit.cs index 1330bba..2573b2d 100644 --- a/Parser/Shared/IsExternalInit.cs +++ b/Parser/Shared/IsExternalInit.cs @@ -4,5 +4,5 @@ namespace System.Runtime.CompilerServices; -internal sealed class IsExternalInit { } +sealed class IsExternalInit { } #endif diff --git a/Processor.Tests/FungeProcessorTests.cs b/Processor.Tests/FungeProcessorTests.cs index b8337a5..c1c6c23 100644 --- a/Processor.Tests/FungeProcessorTests.cs +++ b/Processor.Tests/FungeProcessorTests.cs @@ -1,31 +1,60 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Esolang.Processor; namespace Esolang.Funge.Processor.Tests; [TestClass] public class FungeProcessorTests(TestContext TestContext) { - CancellationToken TestCancellationToken => TestContext.CancellationTokenSource.Token; - private string Run(string source, string? input = null) + CancellationToken TestCancellationToken => TestContext.CancellationToken; + string Run(string source, string? input = null) { var space = Parser.FungeParser.Parse(source); var output = new StringWriter(); var reader = input is null ? TextReader.Null : new StringReader(input); - var proc = new FungeProcessor(space, output, reader); - proc.Run(TestCancellationToken); + var proc = new FungeProcessor(space); + _ = RunToEnd(proc, reader, output, TestCancellationToken); return output.ToString(); } - private int RunGetExitCode(string source) + static int RunToEnd(FungeProcessor proc, TextReader input, TextWriter output, CancellationToken ct) + { + var task = Task.Run(async () => + { + var exitCode = 0; + await foreach (var ev in proc.RunAsyncEnumerable(ct)) + { + switch (ev) + { + case OutputCharEvent oce: output.Write(oce.Output); break; + case OutputIntEvent oie: output.Write(oie.Output); break; + case InputCharEvent ice: + var c = input.Read(); + if (c != -1) ice.Write((char)c); + break; + case InputIntEvent iie: + var line = input.ReadLine(); + if (int.TryParse(line, out var val)) iie.Write(val); + break; + case EndEvent ee: + exitCode = ee.ExitCode; + break; + } + } + return exitCode; + }, ct); + return task.GetAwaiter().GetResult(); + } + + int RunGetExitCode(string source) { var space = Parser.FungeParser.Parse(source); - var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); - return proc.Run(TestCancellationToken); + var proc = new FungeProcessor(space); + return RunToEnd(proc, TextReader.Null, TextWriter.Null, TestCancellationToken); } [TestMethod] [Timeout(Constant.Timeout, CooperativeCancellation = true)] - public async Task TestDirectionalInstructions() + public void TestDirectionalInstructions() { var space = new Parser.FungeSpace(); var pos1 = new Parser.FungeVector(0, 0, 0); @@ -40,14 +69,14 @@ public async Task TestDirectionalInstructions() space[pos4] = 'v'; space[pos5] = '@'; - var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); + var proc = new FungeProcessor(space); var token = TestCancellationToken; - await proc.RunToEndAsync(null, null, token); + RunToEnd(proc, TextReader.Null, TextWriter.Null, token); } - private static string EncodeZeroGnirts(string value) - => $"0\"{new string(value.Reverse().ToArray())}\""; + static string EncodeZeroGnirts(string value) + => $"0\"{new string([.. value.Reverse()])}\""; // ── Termination ──────────────────────────────────────────────────────── @@ -375,21 +404,21 @@ public void RunToEnd_UsesProvidedTextIo() var space = Parser.FungeParser.Parse("&.@"); var output = new StringWriter(); var input = new StringReader("42\n"); - var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); + var proc = new FungeProcessor(space); - var exitCode = proc.RunToEnd(input, output, TestCancellationToken); + var exitCode = RunToEnd(proc, input, output, TestCancellationToken); Assert.AreEqual(0, exitCode); Assert.AreEqual("42 ", output.ToString()); } [TestMethod] - public async Task RunToEndAsync_ReturnsExitCode() + public void RunToEndAsync_ReturnsExitCode() { var space = Parser.FungeParser.Parse("7q"); - var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); + var proc = new FungeProcessor(space); - var exitCode = await proc.RunToEndAsync(cancellationToken: TestCancellationToken); + var exitCode = RunToEnd(proc, TextReader.Null, TextWriter.Null, TestCancellationToken); Assert.AreEqual(7, exitCode); } diff --git a/Processor.Tests/InstructionPointerTests.cs b/Processor.Tests/InstructionPointerTests.cs index f1a5170..e92013b 100644 --- a/Processor.Tests/InstructionPointerTests.cs +++ b/Processor.Tests/InstructionPointerTests.cs @@ -1,5 +1,4 @@ using Esolang.Funge.Parser; -using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Esolang.Funge.Processor.Tests; diff --git a/Processor.Tests/StackStackTests.cs b/Processor.Tests/StackStackTests.cs index fa63105..7ab6841 100644 --- a/Processor.Tests/StackStackTests.cs +++ b/Processor.Tests/StackStackTests.cs @@ -1,5 +1,3 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - namespace Esolang.Funge.Processor.Tests; [TestClass] diff --git a/Processor/FungeProcessor.IEventProcessor.cs b/Processor/FungeProcessor.IEventProcessor.cs index 9cb6110..baa3566 100644 --- a/Processor/FungeProcessor.IEventProcessor.cs +++ b/Processor/FungeProcessor.IEventProcessor.cs @@ -1,63 +1,68 @@ -using Esolang.Processor; -using System.Runtime.CompilerServices; - -namespace Esolang.Funge.Processor; - -public sealed partial class FungeProcessor : IEventProcessor -{ - private sealed class FungeInputCharEvent : InputCharEvent - { - public int? Value { get; private set; } - public override void Write(char c) => Value = c; - } - - private sealed class FungeInputIntEvent : InputIntEvent - { - public int? Value { get; private set; } - public override void Write(int i) => Value = i; - } - - private sealed class FungeState - { - public int ExitCode; - public bool Quit; - public bool SuppressAdvance; - } - - /// - public async IAsyncEnumerable RunAsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var ips = new LinkedList(); - ips.AddFirst(new InstructionPointer(_nextIpId++)); - var state = new FungeState(); - - while (ips.Count > 0 && !state.Quit && !cancellationToken.IsCancellationRequested) - { - var node = ips.First; - while (node is not null && !state.Quit && !cancellationToken.IsCancellationRequested) - { - var nextNode = node.Next; - var ip = node.Value; - - state.SuppressAdvance = false; - foreach (var ev in ExecuteInstruction(ip, ips, node, state)) - { - yield return ev; - } - - if (ip.IsStopped || state.Quit) - { - ips.Remove(node); - } - else if (!state.SuppressAdvance) - { - ip.Position = _space.Advance(ip.Position, ip.Delta); - } - - node = nextNode; - } - } - - yield return new EndEvent(state.ExitCode); - } -} +using Esolang.Processor; +using System.Runtime.CompilerServices; + +namespace Esolang.Funge.Processor; + +public sealed partial class FungeProcessor : IEventProcessor +{ + sealed class FungeInputCharEvent : InputCharEvent + { + public int? Value { get; private set; } + public override void Write(char c) => Value = c; + } + + sealed class FungeInputIntEvent : InputIntEvent + { + public int? Value { get; private set; } + public override void Write(int i) => Value = i; + } + + sealed class FungeState + { + public int ExitCode; + public bool Quit; + public bool SuppressAdvance; + } + + /// + /// Runs the Funge-98 program and returns the process exit code. + /// The program starts with a single IP at (0,0) moving East. + /// + /// Token to cancel execution. + /// Exit code: 0 unless the program used q. + public async IAsyncEnumerable RunAsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var ips = new LinkedList(); + ips.AddFirst(new InstructionPointer(_nextIpId++)); + var state = new FungeState(); + + while (ips.Count > 0 && !state.Quit && !cancellationToken.IsCancellationRequested) + { + var node = ips.First; + while (node is not null && !state.Quit && !cancellationToken.IsCancellationRequested) + { + var nextNode = node.Next; + var ip = node.Value; + + state.SuppressAdvance = false; + foreach (var ev in ExecuteInstruction(ip, ips, node, state)) + { + yield return ev; + } + + if (ip.IsStopped || state.Quit) + { + ips.Remove(node); + } + else if (!state.SuppressAdvance) + { + ip.Position = _space.Advance(ip.Position, ip.Delta); + } + + node = nextNode; + } + } + + yield return new EndEvent(state.ExitCode); + } +} diff --git a/Processor/FungeProcessor.IProcessor.cs b/Processor/FungeProcessor.IProcessor.cs new file mode 100644 index 0000000..50d3b2b --- /dev/null +++ b/Processor/FungeProcessor.IProcessor.cs @@ -0,0 +1,13 @@ + +using Esolang.Funge.Parser; +using Esolang.Processor; +using System.Diagnostics.CodeAnalysis; + +namespace Esolang.Funge.Processor; + +public partial class FungeProcessor : IProcessor +{ + /// + [ExcludeFromCodeCoverage] + FungeSpace IProcessor.Program => _space; +} diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index 3459962..aa1ef71 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -16,52 +16,26 @@ namespace Esolang.Funge.Processor; /// Initializes a new with the given program space and optional I/O. /// /// The parsed Funge-98 program space. -/// Output writer; defaults to . -/// Input reader; defaults to . /// Optional command-line arguments exposed by y. Defaults to host process args. /// Optional environment variable entries (NAME=VALUE) exposed by y. Defaults to host process environment. public sealed partial class FungeProcessor( FungeSpace space, - TextWriter? output = null, - TextReader? input = null, IEnumerable? commandLineArguments = null, - IEnumerable? environmentVariables = null) : IProcessor + IEnumerable? environmentVariables = null) { - private readonly FungeSpace _space = space; - private readonly TextWriter _output = output ?? Console.Out; - private readonly TextReader _input = input ?? Console.In; - private readonly string[] _commandLineArguments = (commandLineArguments ?? Environment.GetCommandLineArgs()) + readonly FungeSpace _space = space; + readonly string[] _commandLineArguments = (commandLineArguments ?? Environment.GetCommandLineArgs()) #pragma warning disable IDE0305 // コレクションの初期化を簡略化します .ToArray(); #pragma warning restore IDE0305 // コレクションの初期化を簡略化します - private readonly string[] _environmentVariables = [.. environmentVariables + readonly string[] _environmentVariables = [.. environmentVariables ?? Environment.GetEnvironmentVariables() .Cast() .Select(static entry => $"{entry.Key}={entry.Value}")]; - private readonly Random _random = new(); - private int _nextIpId; + readonly Random _random = new(); + int _nextIpId; - /// - public FungeSpace Program => _space; - - /// - /// Runs the Funge-98 program and returns the process exit code. - /// The program starts with a single IP at (0,0) moving East. - /// - /// Token to cancel execution. - /// Exit code: 0 unless the program used q. - public int Run(CancellationToken cancellationToken = default) - => RunToEnd(null, null, cancellationToken); - - /// - public int RunToEnd(TextReader? input = null, TextWriter? output = null, CancellationToken cancellationToken = default) - => TextProcessorExtensions.RunToEndAsync(this, input ?? _input, output ?? _output, cancellationToken).AsTask().GetAwaiter().GetResult(); - - /// - public ValueTask RunToEndAsync(TextReader? input = null, TextWriter? output = null, CancellationToken cancellationToken = default) - => TextProcessorExtensions.RunToEndAsync(this, input ?? _input, output ?? _output, cancellationToken); - - private IEnumerable ExecuteInstruction( + IEnumerable ExecuteInstruction( InstructionPointer ip, LinkedList ips, LinkedListNode ipNode, @@ -593,7 +567,7 @@ private IEnumerable ExecuteInstruction( } } - private static FungeVector PopVector(StackStack stack) + static FungeVector PopVector(StackStack stack) { var z = stack.Pop(); var y = stack.Pop(); @@ -601,14 +575,14 @@ private static FungeVector PopVector(StackStack stack) return new FungeVector(x, y, z); } - private static void PushVector(StackStack stack, FungeVector vector) + static void PushVector(StackStack stack, FungeVector vector) { stack.Push(vector.X); stack.Push(vector.Y); stack.Push(vector.Z); } - private static bool TryPopZeroTerminatedString(StackStack stack, out string result) + static bool TryPopZeroTerminatedString(StackStack stack, out string result) { var chars = new List(); while (true) @@ -630,7 +604,7 @@ private static bool TryPopZeroTerminatedString(StackStack stack, out string resu } } - private bool TryInputFile(FungeVector leastPoint, string fileName, bool binaryMode, out FungeVector size) + bool TryInputFile(FungeVector leastPoint, string fileName, bool binaryMode, out FungeVector size) { size = new FungeVector(0, 0, 0); @@ -694,7 +668,7 @@ private bool TryInputFile(FungeVector leastPoint, string fileName, bool binaryMo return true; } - private bool TryOutputFile(FungeVector leastPoint, FungeVector size, string fileName, bool linearText) + bool TryOutputFile(FungeVector leastPoint, FungeVector size, string fileName, bool linearText) { var sx = Math.Max(0, size.X); var sy = Math.Max(0, size.Y); @@ -740,7 +714,7 @@ private bool TryOutputFile(FungeVector leastPoint, FungeVector size, string file } } - private static int ExecuteSystemCommand(string command) + static int ExecuteSystemCommand(string command) { try { @@ -783,7 +757,7 @@ private static int ExecuteSystemCommand(string command) /// If is greater than zero, only item /// (1-indexed from top) is left on the stack. /// - private void PushSysInfo(InstructionPointer ip, int _, int c) + void PushSysInfo(InstructionPointer ip, int _, int c) { // Build list of items in order: items[0] will be last-pushed (item 1 from top) List items = []; @@ -829,9 +803,9 @@ private void PushSysInfo(InstructionPointer ip, int _, int c) var now = DateTime.Now; // 20. Current date: (year-1900)*256*256 + month*256 + day - items.Add(((now.Year - 1900) * 256 * 256) + (now.Month * 256) + now.Day); + items.Add((now.Year - 1900) * 256 * 256 + now.Month * 256 + now.Day); // 21. Current time: HH*256*256 + MM*256 + SS - items.Add((now.Hour * 256 * 256) + (now.Minute * 256) + now.Second); + items.Add(now.Hour * 256 * 256 + now.Minute * 256 + now.Second); // 22. Number of stacks in stack stack items.Add(ip.StackStack.StackCount); // 23+. Size of each stack (TOSS first) diff --git a/Processor/InstructionPointer.cs b/Processor/InstructionPointer.cs index 3dec861..5cf19bc 100644 --- a/Processor/InstructionPointer.cs +++ b/Processor/InstructionPointer.cs @@ -31,7 +31,7 @@ public sealed class InstructionPointer /// Initializes an IP with the given ID and a new empty stack stack. public InstructionPointer(int id) : this(id, new StackStack()) { } - private InstructionPointer(int id, StackStack stackStack) + InstructionPointer(int id, StackStack stackStack) { Id = id; StackStack = stackStack; diff --git a/Processor/README.md b/Processor/README.md index 3addb62..b6781c9 100644 --- a/Processor/README.md +++ b/Processor/README.md @@ -65,12 +65,14 @@ using Esolang.Funge.Parser; using Esolang.Funge.Processor; var space = FungeParser.ParseFile("hello.b98"); -var proc = new FungeProcessor(space, Console.Out, Console.In); -int exitCode = proc.Run(); +var proc = new FungeProcessor(space); +int exitCode = await proc.RunToEndAsync(); ``` -`FungeProcessor` accepts optional `TextWriter` (output) and `TextReader` (input) arguments, defaulting to `Console.Out` / `Console.In`. -`Run()` accepts an optional `CancellationToken` and returns the exit code set by `q` (0 if not used). +`FungeProcessor` executes programs via an event stream. You can run it to completion using `RunToEndAsync()` (or the synchronous `Run()`), which defaults to `Console.In` / `Console.Out`. +For fine-grained control, use `RunAsyncEnumerable()` to handle I/O events manually. + +`Run()` and `RunToEndAsync()` accept optional `TextReader` and `TextWriter` arguments, and an optional `CancellationToken`. They return the exit code set by `q` (0 if not used). ## References diff --git a/Processor/StackStack.cs b/Processor/StackStack.cs index d101cde..884fc43 100644 --- a/Processor/StackStack.cs +++ b/Processor/StackStack.cs @@ -7,12 +7,12 @@ namespace Esolang.Funge.Processor; /// public sealed class StackStack { - private readonly LinkedList> _stacks = new(); + readonly LinkedList> _stacks = new(); /// Initializes a new stack stack with a single empty TOSS. public StackStack() => _stacks.AddFirst(new Stack()); - private StackStack(LinkedList> stacks) => _stacks = stacks; + StackStack(LinkedList> stacks) => _stacks = stacks; /// Gets the top-of-stack-stack (current active stack). public Stack TOSS => _stacks.First!.Value; diff --git a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs index b6abc91..9f8c47a 100644 --- a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs +++ b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs @@ -13,7 +13,7 @@ Console.WriteLine($"{nameof(FungeSample.HelloWorldWriter)}: {textWriter}"); // IEnumerable (file-based) -Console.WriteLine($"{nameof(FungeSample.HelloWorldBytes)}: {Encoding.UTF8.GetString(FungeSample.HelloWorldBytes().ToArray())}"); +Console.WriteLine($"{nameof(FungeSample.HelloWorldBytes)}: {Encoding.UTF8.GetString([.. FungeSample.HelloWorldBytes()])}"); // IAsyncEnumerable (file-based) Console.WriteLine($"{nameof(FungeSample.HelloWorldBytesAsync)}: {Encoding.UTF8.GetString(await ToByteArrayAsync(FungeSample.HelloWorldBytesAsync()))}"); From 5eaea98fe7e8d6a35344b8e2c9ead768be45a763 Mon Sep 17 00:00:00 2001 From: juner Date: Mon, 1 Jun 2026 17:49:26 +0900 Subject: [PATCH 18/25] Esolang.Abstractions update --- .../BindingError.cs | 220 ++++---- .../KnownTypes.cs | 499 +++++++++--------- .../MethodInputKind.cs | 34 +- .../MethodOutputKind.cs | 41 +- .../MethodReturnKind.cs | 76 +-- .../MethodSignatureBinder.cs | 456 ++++++++-------- .../MethodSignatureBinding.cs | 196 +++---- .../.editorconfig | 2 + .../InterpreterExtensions.cs | 59 +++ Interpreter/FungeInterpreterExtensions.cs | 32 +- Processor.Tests/FungeProcessorTests.cs | 2 +- .../Esolang.Processor.Abstractions/IOEvent.cs | 110 ++++ .../IProcessor.cs | 138 ++--- .../PipeProcessorExtensions.cs | 112 ---- .../TextProcessorExtensions.cs | 112 ---- Processor/FungeProcessor.IEventProcessor.cs | 12 +- Processor/FungeProcessor.cs | 17 +- 17 files changed, 978 insertions(+), 1140 deletions(-) create mode 100644 Interpreter/Esolang.Interpreter.Abstractions/.editorconfig create mode 100644 Interpreter/Esolang.Interpreter.Abstractions/InterpreterExtensions.cs create mode 100644 Processor/Esolang.Processor.Abstractions/IOEvent.cs delete mode 100644 Processor/Esolang.Processor.Abstractions/PipeProcessorExtensions.cs delete mode 100644 Processor/Esolang.Processor.Abstractions/TextProcessorExtensions.cs diff --git a/Generator/Esolang.Generator.Abstractions/BindingError.cs b/Generator/Esolang.Generator.Abstractions/BindingError.cs index 84ab00d..3d8e89f 100644 --- a/Generator/Esolang.Generator.Abstractions/BindingError.cs +++ b/Generator/Esolang.Generator.Abstractions/BindingError.cs @@ -1,110 +1,110 @@ -#nullable enable -using Microsoft.CodeAnalysis; -using System.Diagnostics.CodeAnalysis; - -namespace Esolang.Generator; - -/// -/// Specifies the kind of diagnostic error that occurred during method signature binding. -/// -public enum BindingErrorKind -{ - /// The return type of the method is not supported. - UnsupportedReturnType, - /// A parameter has an invalid modifier (e.g., ref, out, in). - InvalidParameterModifier, - /// More than one parameter is competing for the same input role. - DuplicateInput, - /// More than one parameter is competing for the same output role. - DuplicateOutput, - /// More than one cancellation token parameter was found. - DuplicateCancellationToken, - /// More than one logger parameter was found. - DuplicateLogger, - /// A conflict exists between the return type and an output parameter. - ReturnOutputConflict, -} - -/// -/// Represents a diagnostic error that occurred during method signature binding. -/// -[ExcludeFromCodeCoverage] -public abstract record BindingError -{ - /// - /// - /// - /// The kind of error. - /// The location associated with the error. - BindingError(BindingErrorKind Kind, Location? Location) : base() - => (this.Kind, this.Location) = (Kind, Location); - - /// - /// The kind of error. - /// - public BindingErrorKind Kind { get; } - - /// - /// The location associated with the error. - /// - public Location? Location { get; } - - /// - /// The return type of the method is not supported. - /// - /// The unsupported return type symbol. - /// The location of the return type. - public sealed record UnsupportedReturnType(ITypeSymbol ReturnType, Location? Location) - : BindingError(BindingErrorKind.UnsupportedReturnType, Location); - - /// - /// A parameter has an invalid modifier (e.g., ref, out, in). - /// - /// The parameter with the invalid modifier. - /// The location of the parameter. - public sealed record InvalidParameterModifier(IParameterSymbol Parameter, Location? Location) - : BindingError(BindingErrorKind.InvalidParameterModifier, Location); - - /// - /// More than one parameter is competing for the same input role. - /// - /// The parameter that caused the duplication. - /// The kind of input that was already assigned. - /// The location of the duplicate parameter. - public sealed record DuplicateInput(IParameterSymbol Parameter, MethodInputKind ExistingKind, Location? Location) - : BindingError(BindingErrorKind.DuplicateInput, Location); - - /// - /// More than one parameter is competing for the same output role. - /// - /// The parameter that caused the duplication. - /// The kind of output that was already assigned. - /// The location of the duplicate parameter. - public sealed record DuplicateOutput(IParameterSymbol Parameter, MethodOutputKind ExistingKind, Location? Location) - : BindingError(BindingErrorKind.DuplicateOutput, Location); - - /// - /// More than one cancellation token parameter was found. - /// - /// The duplicate cancellation token parameter. - /// The location of the duplicate parameter. - public sealed record DuplicateCancellationToken(IParameterSymbol Parameter, Location? Location) - : BindingError(BindingErrorKind.DuplicateCancellationToken, Location); - - /// - /// More than one logger parameter was found. - /// - /// The duplicate logger parameter. - /// The location of the duplicate parameter. - public sealed record DuplicateLogger(IParameterSymbol Parameter, Location? Location) - : BindingError(BindingErrorKind.DuplicateLogger, Location); - - /// - /// A conflict exists between the return type and an output parameter. - /// - /// The output parameter that conflicts with the return type. - /// The location of the conflicting parameter. - public sealed record ReturnOutputConflict(IParameterSymbol Parameter, Location? Location) - : BindingError(BindingErrorKind.ReturnOutputConflict, Location); - -} +#nullable enable +using Microsoft.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; + +namespace Esolang.Generator; + +/// +/// Specifies the kind of diagnostic error that occurred during method signature binding. +/// +public enum BindingErrorKind +{ + /// The return type of the method is not supported. + UnsupportedReturnType, + /// A parameter has an invalid modifier (e.g., ref, out, in). + InvalidParameterModifier, + /// More than one parameter is competing for the same input role. + DuplicateInput, + /// More than one parameter is competing for the same output role. + DuplicateOutput, + /// More than one cancellation token parameter was found. + DuplicateCancellationToken, + /// More than one logger parameter was found. + DuplicateLogger, + /// A conflict exists between the return type and an output parameter. + ReturnOutputConflict, +} + +/// +/// Represents a diagnostic error that occurred during method signature binding. +/// +[ExcludeFromCodeCoverage] +public abstract record BindingError +{ + /// + /// + /// + /// The kind of error. + /// The location associated with the error. + BindingError(BindingErrorKind Kind, Location? Location) : base() + => (this.Kind, this.Location) = (Kind, Location); + + /// + /// The kind of error. + /// + public BindingErrorKind Kind { get; } + + /// + /// The location associated with the error. + /// + public Location? Location { get; } + + /// + /// The return type of the method is not supported. + /// + /// The unsupported return type symbol. + /// The location of the return type. + public sealed record UnsupportedReturnType(ITypeSymbol ReturnType, Location? Location) + : BindingError(BindingErrorKind.UnsupportedReturnType, Location); + + /// + /// A parameter has an invalid modifier (e.g., ref, out, in). + /// + /// The parameter with the invalid modifier. + /// The location of the parameter. + public sealed record InvalidParameterModifier(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.InvalidParameterModifier, Location); + + /// + /// More than one parameter is competing for the same input role. + /// + /// The parameter that caused the duplication. + /// The kind of input that was already assigned. + /// The location of the duplicate parameter. + public sealed record DuplicateInput(IParameterSymbol Parameter, MethodInputKind ExistingKind, Location? Location) + : BindingError(BindingErrorKind.DuplicateInput, Location); + + /// + /// More than one parameter is competing for the same output role. + /// + /// The parameter that caused the duplication. + /// The kind of output that was already assigned. + /// The location of the duplicate parameter. + public sealed record DuplicateOutput(IParameterSymbol Parameter, MethodOutputKind ExistingKind, Location? Location) + : BindingError(BindingErrorKind.DuplicateOutput, Location); + + /// + /// More than one cancellation token parameter was found. + /// + /// The duplicate cancellation token parameter. + /// The location of the duplicate parameter. + public sealed record DuplicateCancellationToken(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.DuplicateCancellationToken, Location); + + /// + /// More than one logger parameter was found. + /// + /// The duplicate logger parameter. + /// The location of the duplicate parameter. + public sealed record DuplicateLogger(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.DuplicateLogger, Location); + + /// + /// A conflict exists between the return type and an output parameter. + /// + /// The output parameter that conflicts with the return type. + /// The location of the conflicting parameter. + public sealed record ReturnOutputConflict(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.ReturnOutputConflict, Location); + +} diff --git a/Generator/Esolang.Generator.Abstractions/KnownTypes.cs b/Generator/Esolang.Generator.Abstractions/KnownTypes.cs index add5e5c..f280325 100644 --- a/Generator/Esolang.Generator.Abstractions/KnownTypes.cs +++ b/Generator/Esolang.Generator.Abstractions/KnownTypes.cs @@ -1,249 +1,250 @@ -#nullable enable -using Microsoft.CodeAnalysis; -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace Esolang.Generator; - -/// -/// Holds resolved type symbols for a compilation. -/// -/// -/// Initializes a new instance of the struct. -/// -/// The compilation to resolve types from. -[ExcludeFromCodeCoverage] -public readonly struct KnownTypes(Compilation compilation) -{ - /// The string type symbol. - public readonly INamedTypeSymbol? String = compilation.GetSpecialType(SpecialType.System_String); - /// The byte type symbol. - public readonly INamedTypeSymbol? Byte = compilation.GetSpecialType(SpecialType.System_Byte); - /// The int type symbol. - public readonly INamedTypeSymbol? Int32 = compilation.GetSpecialType(SpecialType.System_Int32); - /// The System.Threading.Tasks.Task type symbol. - public readonly INamedTypeSymbol? Task = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task"); - /// The System.Threading.Tasks.Task{TResult} type symbol. - public readonly INamedTypeSymbol? TaskT = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1"); - /// The System.Threading.Tasks.ValueTask type symbol. - public readonly INamedTypeSymbol? ValueTask = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask"); - /// The System.Threading.Tasks.ValueTask{TResult} type symbol. - public readonly INamedTypeSymbol? ValueTaskT = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); - /// The System.Collections.Generic.IEnumerable{T} type symbol. - public readonly INamedTypeSymbol? IEnumerableT = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IEnumerable`1"); - /// The System.Collections.Generic.IAsyncEnumerable{T} type symbol. - public readonly INamedTypeSymbol? IAsyncEnumerableT = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1"); - /// The System.IO.Pipelines.PipeReader type symbol. - public readonly INamedTypeSymbol? PipeReader = compilation.GetBestTypeByMetadataName("System.IO.Pipelines.PipeReader"); - /// The System.IO.Pipelines.PipeWriter type symbol. - public readonly INamedTypeSymbol? PipeWriter = compilation.GetBestTypeByMetadataName("System.IO.Pipelines.PipeWriter"); - /// The System.IO.TextReader type symbol. - public readonly INamedTypeSymbol? TextReader = compilation.GetBestTypeByMetadataName("System.IO.TextReader"); - /// The System.IO.TextWriter type symbol. - public readonly INamedTypeSymbol? TextWriter = compilation.GetBestTypeByMetadataName("System.IO.TextWriter"); - /// The System.Threading.CancellationToken type symbol. - public readonly INamedTypeSymbol? CancellationToken = compilation.GetBestTypeByMetadataName("System.Threading.CancellationToken"); - /// The Microsoft.Extensions.Logging.ILogger type symbol. - public readonly INamedTypeSymbol? ILogger = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger"); - /// The Microsoft.Extensions.Logging.ILogger{T} type symbol. - public readonly INamedTypeSymbol? ILoggerT = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger`1"); - - static bool EqualsDefinition(ITypeSymbol? type, ISymbol? symbol) => - type != null && symbol != null && SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, symbol); - - static bool EqualsType(ITypeSymbol? type, ISymbol? symbol) => - type != null && symbol != null && SymbolEqualityComparer.Default.Equals(type, symbol); - - /// Gets a value indicating whether the type is string. - /// The type to check. - /// Optional: Whether to check for nullability. - public readonly bool IsString(ITypeSymbol? type, bool? isNullable = null) - { - if (type is not INamedTypeSymbol named || !SymbolEqualityComparer.Default.Equals(named, String)) return false; - if (isNullable == null) return true; - if (isNullable.Value) return type.NullableAnnotation == NullableAnnotation.Annotated; - return type.NullableAnnotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; - } - - /// Gets a value indicating whether the type is byte. - public readonly bool IsByte(ITypeSymbol? type) => EqualsType(type, Byte); - /// Gets a value indicating whether the type is int. - public readonly bool IsInt32(ITypeSymbol? type) => EqualsType(type, Int32); - - /// Gets a value indicating whether the type is System.Threading.Tasks.Task. - public readonly bool IsTask(ITypeSymbol? type) => EqualsType(type, Task); - /// Gets a value indicating whether the type is System.Threading.Tasks.Task{TResult}. - public readonly bool IsTaskT(ITypeSymbol? type, bool? isNullable = null) - { - if (type is not INamedTypeSymbol named || !EqualsDefinition(named, TaskT)) return false; - if (isNullable == null) return true; - var annotation = named.TypeArguments[0].NullableAnnotation; - return isNullable.Value ? annotation == NullableAnnotation.Annotated : annotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; - } - /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask. - public readonly bool IsValueTask(ITypeSymbol? type) => EqualsType(type, ValueTask); - /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{TResult}. - public readonly bool IsValueTaskT(ITypeSymbol? type, bool? isNullable = null) - { - if (type is not INamedTypeSymbol named || !EqualsDefinition(named, ValueTaskT)) return false; - if (isNullable == null) return true; - var annotation = named.TypeArguments[0].NullableAnnotation; - return isNullable.Value ? annotation == NullableAnnotation.Annotated : annotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; - } - /// Gets a value indicating whether the type is System.Collections.Generic.IEnumerable{T}. - public readonly bool IsIEnumerableT(ITypeSymbol? type) => EqualsDefinition(type, IEnumerableT); - /// Gets a value indicating whether the type is System.Collections.Generic.IAsyncEnumerable{T}. - public readonly bool IsIAsyncEnumerableT(ITypeSymbol? type) => EqualsDefinition(type, IAsyncEnumerableT); - - /// Gets a value indicating whether the type is System.IO.Pipelines.PipeReader. - public readonly bool IsPipeReader(ITypeSymbol? type) => EqualsType(type, PipeReader); - /// Gets a value indicating whether the type is System.IO.Pipelines.PipeWriter. - public readonly bool IsPipeWriter(ITypeSymbol? type) => EqualsType(type, PipeWriter); - /// Gets a value indicating whether the type is System.IO.TextReader. - public readonly bool IsTextReader(ITypeSymbol? type) => EqualsType(type, TextReader); - /// Gets a value indicating whether the type is System.IO.TextWriter. - public readonly bool IsTextWriter(ITypeSymbol? type) => EqualsType(type, TextWriter); - /// Gets a value indicating whether the type is System.Threading.CancellationToken. - public readonly bool IsCancellationToken(ITypeSymbol? type) => EqualsType(type, CancellationToken); - - /// Gets a value indicating whether the type is System.Threading.Tasks.Task{String}. - public readonly bool IsTaskString(ITypeSymbol? type, bool? isNullable = null) => IsTaskT(type, isNullable) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_String; - /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{String}. - public readonly bool IsValueTaskString(ITypeSymbol? type, bool? isNullable = null) => IsValueTaskT(type, isNullable) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_String; - - /// Gets a value indicating whether the type is System.Threading.Tasks.Task{Int32}. - public readonly bool IsTaskInt32(ITypeSymbol? type) => IsTaskT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Int32; - /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{Int32}. - public readonly bool IsValueTaskInt32(ITypeSymbol? type) => IsValueTaskT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Int32; - - /// Gets a value indicating whether the type is System.Collections.Generic.IEnumerable{Byte}. - public readonly bool IsIEnumerableByte(ITypeSymbol? type) => IsIEnumerableT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Byte; - /// Gets a value indicating whether the type is System.Collections.Generic.IAsyncEnumerable{Byte}. - public readonly bool IsIAsyncEnumerableByte(ITypeSymbol? type) => IsIAsyncEnumerableT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Byte; - - /// Gets a value indicating whether the type is a logger type (ILogger or ILogger{T}). - public readonly bool IsLogger(ITypeSymbol? type) - { - if (type == null) return false; - if (EqualsType(type, ILogger) || EqualsDefinition(type, ILoggerT)) return true; - foreach (var iface in type.AllInterfaces) - { - if (EqualsType(iface, ILogger) || EqualsDefinition(iface, ILoggerT)) return true; - } - return false; - } - - [ExcludeFromCodeCoverage] - readonly bool PrintMembers(StringBuilder builder) - { - builder.Append(nameof(String)).Append('='); - AppendNamedTypeSymbol(String, builder); - builder.Append(", "); - - builder.Append(nameof(Byte)).Append('='); - AppendNamedTypeSymbol(Byte, builder); - builder.Append(", "); - - builder.Append(nameof(Int32)).Append('='); - AppendNamedTypeSymbol(Int32, builder); - builder.Append(", "); - - builder.Append(nameof(Task)).Append('='); - AppendNamedTypeSymbol(Task, builder); - builder.Append(", "); - - builder.Append(nameof(TaskT)).Append('='); - AppendNamedTypeSymbol(TaskT, builder); - builder.Append(", "); - - builder.Append(nameof(ValueTask)).Append('='); - AppendNamedTypeSymbol(ValueTask, builder); - builder.Append(", "); - - builder.Append(nameof(ValueTaskT)).Append('='); - AppendNamedTypeSymbol(ValueTaskT, builder); - builder.Append(", "); - - builder.Append(nameof(IEnumerableT)).Append('='); - AppendNamedTypeSymbol(IEnumerableT, builder); - builder.Append(", "); - - builder.Append(nameof(IAsyncEnumerableT)).Append('='); - AppendNamedTypeSymbol(IAsyncEnumerableT, builder); - builder.Append(", "); - - builder.Append(nameof(PipeReader)).Append('='); - AppendNamedTypeSymbol(PipeReader, builder); - builder.Append(", "); - - builder.Append(nameof(PipeWriter)).Append('='); - AppendNamedTypeSymbol(PipeWriter, builder); - builder.Append(", "); - - builder.Append(nameof(TextReader)).Append('='); - AppendNamedTypeSymbol(TextReader, builder); - builder.Append(", "); - - builder.Append(nameof(TextWriter)).Append('='); - AppendNamedTypeSymbol(TextWriter, builder); - builder.Append(", "); - - builder.Append(nameof(CancellationToken)).Append('='); - AppendNamedTypeSymbol(CancellationToken, builder); - builder.Append(", "); - - builder.Append(nameof(ILogger)).Append('='); - AppendNamedTypeSymbol(ILogger, builder); - builder.Append(", "); - - builder.Append(nameof(ILoggerT)).Append('='); - AppendNamedTypeSymbol(ILoggerT, builder); - - return true; - static void AppendNamedTypeSymbol(INamedTypeSymbol? symbol, StringBuilder builder) - { - if (symbol == null) return; - builder.Append('('); - builder.Append(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - builder.Append(", "); - builder.Append(nameof(symbol.NullableAnnotation)).Append('=').Append(symbol.NullableAnnotation); - builder.Append(')'); - } - } - - /// - [ExcludeFromCodeCoverage] - public override string ToString() - { - var builder = new StringBuilder(); - builder.Append(nameof(KnownTypes)).Append(" {"); - if (!PrintMembers(builder)) - { - builder.Append(' '); - } - builder.Append('}'); - return builder.ToString(); - } -} - -/// -/// Provides utility methods for resolving types from a . -/// -public static class TypeResolutionExtensions -{ - /// - /// Resolves the best for the specified metadata name. - /// - public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, string metadataName) - { - var type = compilation.GetTypeByMetadataName(metadataName); - if (type != null) return type; - - foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols) - { - var found = assembly.GetTypeByMetadataName(metadataName); - if (found != null) return found; - } - return null; - } -} +#nullable enable +using Microsoft.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Esolang.Generator; + +/// +/// Holds resolved type symbols for a compilation. +/// +/// +/// Initializes a new instance of the struct. +/// +/// The compilation to resolve types from. +[ExcludeFromCodeCoverage] +public readonly struct KnownTypes(Compilation compilation) +{ + /// The string type symbol. + public readonly INamedTypeSymbol? String = compilation.GetSpecialType(SpecialType.System_String); + /// The byte type symbol. + public readonly INamedTypeSymbol? Byte = compilation.GetSpecialType(SpecialType.System_Byte); + /// The int type symbol. + public readonly INamedTypeSymbol? Int32 = compilation.GetSpecialType(SpecialType.System_Int32); + /// The System.Threading.Tasks.Task type symbol. + public readonly INamedTypeSymbol? Task = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task"); + /// The System.Threading.Tasks.Task{TResult} type symbol. + public readonly INamedTypeSymbol? TaskT = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1"); + /// The System.Threading.Tasks.ValueTask type symbol. + public readonly INamedTypeSymbol? ValueTask = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask"); + /// The System.Threading.Tasks.ValueTask{TResult} type symbol. + public readonly INamedTypeSymbol? ValueTaskT = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); + /// The System.Collections.Generic.IEnumerable{T} type symbol. + public readonly INamedTypeSymbol? IEnumerableT = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IEnumerable`1"); + /// The System.Collections.Generic.IAsyncEnumerable{T} type symbol. + public readonly INamedTypeSymbol? IAsyncEnumerableT = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1"); + /// The System.IO.Pipelines.PipeReader type symbol. + public readonly INamedTypeSymbol? PipeReader = compilation.GetBestTypeByMetadataName("System.IO.Pipelines.PipeReader"); + /// The System.IO.Pipelines.PipeWriter type symbol. + public readonly INamedTypeSymbol? PipeWriter = compilation.GetBestTypeByMetadataName("System.IO.Pipelines.PipeWriter"); + /// The System.IO.TextReader type symbol. + public readonly INamedTypeSymbol? TextReader = compilation.GetBestTypeByMetadataName("System.IO.TextReader"); + /// The System.IO.TextWriter type symbol. + public readonly INamedTypeSymbol? TextWriter = compilation.GetBestTypeByMetadataName("System.IO.TextWriter"); + /// The System.Threading.CancellationToken type symbol. + public readonly INamedTypeSymbol? CancellationToken = compilation.GetBestTypeByMetadataName("System.Threading.CancellationToken"); + /// The Microsoft.Extensions.Logging.ILogger type symbol. + public readonly INamedTypeSymbol? ILogger = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger"); + /// The Microsoft.Extensions.Logging.ILogger{T} type symbol. + public readonly INamedTypeSymbol? ILoggerT = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger`1"); + + static bool EqualsDefinition(ITypeSymbol? type, ISymbol? symbol) => + type != null && symbol != null && SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, symbol); + + static bool EqualsType(ITypeSymbol? type, ISymbol? symbol) => + type != null && symbol != null && SymbolEqualityComparer.Default.Equals(type, symbol); + + /// Gets a value indicating whether the type is string. + /// The type to check. + /// Optional: Whether to check for nullability. + public readonly bool IsString(ITypeSymbol? type, bool? isNullable = null) + { + if (type is not INamedTypeSymbol named || !SymbolEqualityComparer.Default.Equals(named, String)) return false; + if (isNullable == null) return true; + if (isNullable.Value) return type.NullableAnnotation == NullableAnnotation.Annotated; + return type.NullableAnnotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; + } + + /// Gets a value indicating whether the type is byte. + public readonly bool IsByte(ITypeSymbol? type) => EqualsType(type, Byte); + /// Gets a value indicating whether the type is int. + public readonly bool IsInt32(ITypeSymbol? type) => EqualsType(type, Int32); + + /// Gets a value indicating whether the type is System.Threading.Tasks.Task. + public readonly bool IsTask(ITypeSymbol? type) => EqualsType(type, Task); + /// Gets a value indicating whether the type is System.Threading.Tasks.Task{TResult}. + public readonly bool IsTaskT(ITypeSymbol? type, bool? isNullable = null) + { + if (type is not INamedTypeSymbol named || !EqualsDefinition(named, TaskT)) return false; + if (isNullable == null) return true; + var annotation = named.TypeArguments[0].NullableAnnotation; + return isNullable.Value ? annotation == NullableAnnotation.Annotated : annotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; + } + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask. + public readonly bool IsValueTask(ITypeSymbol? type) => EqualsType(type, ValueTask); + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{TResult}. + public readonly bool IsValueTaskT(ITypeSymbol? type, bool? isNullable = null) + { + if (type is not INamedTypeSymbol named || !EqualsDefinition(named, ValueTaskT)) return false; + if (isNullable == null) return true; + var annotation = named.TypeArguments[0].NullableAnnotation; + return isNullable.Value ? annotation == NullableAnnotation.Annotated : annotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; + } + /// Gets a value indicating whether the type is System.Collections.Generic.IEnumerable{T}. + public readonly bool IsIEnumerableT(ITypeSymbol? type) => EqualsDefinition(type, IEnumerableT); + /// Gets a value indicating whether the type is System.Collections.Generic.IAsyncEnumerable{T}. + public readonly bool IsIAsyncEnumerableT(ITypeSymbol? type) => EqualsDefinition(type, IAsyncEnumerableT); + + /// Gets a value indicating whether the type is System.IO.Pipelines.PipeReader. + public readonly bool IsPipeReader(ITypeSymbol? type) => EqualsType(type, PipeReader); + /// Gets a value indicating whether the type is System.IO.Pipelines.PipeWriter. + public readonly bool IsPipeWriter(ITypeSymbol? type) => EqualsType(type, PipeWriter); + /// Gets a value indicating whether the type is System.IO.TextReader. + public readonly bool IsTextReader(ITypeSymbol? type) => EqualsType(type, TextReader); + /// Gets a value indicating whether the type is System.IO.TextWriter. + public readonly bool IsTextWriter(ITypeSymbol? type) => EqualsType(type, TextWriter); + /// Gets a value indicating whether the type is System.Threading.CancellationToken. + public readonly bool IsCancellationToken(ITypeSymbol? type) => EqualsType(type, CancellationToken); + + /// Gets a value indicating whether the type is System.Threading.Tasks.Task{String}. + public readonly bool IsTaskString(ITypeSymbol? type, bool? isNullable = null) => IsTaskT(type, isNullable) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_String; + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{String}. + public readonly bool IsValueTaskString(ITypeSymbol? type, bool? isNullable = null) => IsValueTaskT(type, isNullable) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_String; + + /// Gets a value indicating whether the type is System.Threading.Tasks.Task{Int32}. + public readonly bool IsTaskInt32(ITypeSymbol? type) => IsTaskT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Int32; + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{Int32}. + public readonly bool IsValueTaskInt32(ITypeSymbol? type) => IsValueTaskT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Int32; + + /// Gets a value indicating whether the type is System.Collections.Generic.IEnumerable{Byte}. + public readonly bool IsIEnumerableByte(ITypeSymbol? type) => IsIEnumerableT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Byte; + /// Gets a value indicating whether the type is System.Collections.Generic.IAsyncEnumerable{Byte}. + public readonly bool IsIAsyncEnumerableByte(ITypeSymbol? type) => IsIAsyncEnumerableT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Byte; + + /// Gets a value indicating whether the type is a logger type (ILogger or ILogger{T}). + public readonly bool IsLogger(ITypeSymbol? type) + { + if (type == null) return false; + if (EqualsType(type, ILogger) || EqualsDefinition(type, ILoggerT)) return true; + foreach (var iface in type.AllInterfaces) + { + if (EqualsType(iface, ILogger) || EqualsDefinition(iface, ILoggerT)) return true; + } + return false; + } + + [ExcludeFromCodeCoverage] + readonly bool PrintMembers(StringBuilder builder) + { + builder.Append(nameof(String)).Append('='); + AppendNamedTypeSymbol(String, builder); + builder.Append(", "); + + builder.Append(nameof(Byte)).Append('='); + AppendNamedTypeSymbol(Byte, builder); + builder.Append(", "); + + builder.Append(nameof(Int32)).Append('='); + AppendNamedTypeSymbol(Int32, builder); + builder.Append(", "); + + builder.Append(nameof(Task)).Append('='); + AppendNamedTypeSymbol(Task, builder); + builder.Append(", "); + + builder.Append(nameof(TaskT)).Append('='); + AppendNamedTypeSymbol(TaskT, builder); + builder.Append(", "); + + builder.Append(nameof(ValueTask)).Append('='); + AppendNamedTypeSymbol(ValueTask, builder); + builder.Append(", "); + + builder.Append(nameof(ValueTaskT)).Append('='); + AppendNamedTypeSymbol(ValueTaskT, builder); + builder.Append(", "); + + builder.Append(nameof(IEnumerableT)).Append('='); + AppendNamedTypeSymbol(IEnumerableT, builder); + builder.Append(", "); + + builder.Append(nameof(IAsyncEnumerableT)).Append('='); + AppendNamedTypeSymbol(IAsyncEnumerableT, builder); + builder.Append(", "); + + builder.Append(nameof(PipeReader)).Append('='); + AppendNamedTypeSymbol(PipeReader, builder); + builder.Append(", "); + + builder.Append(nameof(PipeWriter)).Append('='); + AppendNamedTypeSymbol(PipeWriter, builder); + builder.Append(", "); + + builder.Append(nameof(TextReader)).Append('='); + AppendNamedTypeSymbol(TextReader, builder); + builder.Append(", "); + + builder.Append(nameof(TextWriter)).Append('='); + AppendNamedTypeSymbol(TextWriter, builder); + builder.Append(", "); + + builder.Append(nameof(CancellationToken)).Append('='); + AppendNamedTypeSymbol(CancellationToken, builder); + builder.Append(", "); + + builder.Append(nameof(ILogger)).Append('='); + AppendNamedTypeSymbol(ILogger, builder); + builder.Append(", "); + + builder.Append(nameof(ILoggerT)).Append('='); + AppendNamedTypeSymbol(ILoggerT, builder); + + return true; + static void AppendNamedTypeSymbol(INamedTypeSymbol? symbol, StringBuilder builder) + { + if (symbol == null) return; + builder.Append('('); + builder.Append(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + builder.Append(", "); + builder.Append(nameof(symbol.NullableAnnotation)).Append('=').Append(symbol.NullableAnnotation); + builder.Append(')'); + } + } + + /// + [ExcludeFromCodeCoverage] + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append(nameof(KnownTypes)).Append(" {"); + if (!PrintMembers(builder)) + { + builder.Append(' '); + } + builder.Append('}'); + return builder.ToString(); + } +} + +/// +/// Provides utility methods for resolving types from a . +/// +[ExcludeFromCodeCoverage] +public static class TypeResolutionExtensions +{ + /// + /// Resolves the best for the specified metadata name. + /// + public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, string metadataName) + { + var type = compilation.GetTypeByMetadataName(metadataName); + if (type != null) return type; + + foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols) + { + var found = assembly.GetTypeByMetadataName(metadataName); + if (found != null) return found; + } + return null; + } +} diff --git a/Generator/Esolang.Generator.Abstractions/MethodInputKind.cs b/Generator/Esolang.Generator.Abstractions/MethodInputKind.cs index d22a6f7..59f5282 100644 --- a/Generator/Esolang.Generator.Abstractions/MethodInputKind.cs +++ b/Generator/Esolang.Generator.Abstractions/MethodInputKind.cs @@ -1,17 +1,17 @@ -#nullable enable -namespace Esolang.Generator; - -/// -/// Specifies the input mechanism of the generated method. -/// -public enum MethodInputKind -{ - /// No explicit input mechanism. - None, - /// Input is provided via a string parameter. - String, - /// Input is provided via a TextReader parameter. - TextReader, - /// Input is provided via a PipeReader parameter. - PipeReader, -} +#nullable enable +namespace Esolang.Generator; + +/// +/// Specifies the input mechanism of the generated method. +/// +public enum MethodInputKind +{ + /// No explicit input mechanism. + None, + /// Input is provided via a string parameter. + String, + /// Input is provided via a TextReader parameter. + TextReader, + /// Input is provided via a PipeReader parameter. + PipeReader, +} diff --git a/Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs b/Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs index 269af57..4d48f81 100644 --- a/Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs +++ b/Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs @@ -1,21 +1,20 @@ -#nullable enable -namespace Esolang.Generator; - -/// -/// Specifies the output mechanism of the generated method. -/// -public enum MethodOutputKind -{ - /// No explicit output mechanism. - None, - /// Output is written to a TextWriter parameter. - TextWriter, - /// Output is written to a PipeWriter parameter. - PipeWriter, - /// Output is returned as a string. - ReturnString, - /// Output is yielded via IEnumerable<byte>. - ReturnIEnumerable, - /// Output is yielded via IAsyncEnumerable<byte>. - ReturnIAsyncEnumerable, -} +namespace Esolang.Generator; + +/// +/// Specifies the output mechanism of the generated method. +/// +public enum MethodOutputKind +{ + /// No explicit output mechanism. + None, + /// Output is written to a TextWriter parameter. + TextWriter, + /// Output is written to a PipeWriter parameter. + PipeWriter, + /// Output is returned as a string. + ReturnString, + /// Output is yielded via IEnumerable<byte>. + ReturnIEnumerable, + /// Output is yielded via IAsyncEnumerable<byte>. + ReturnIAsyncEnumerable, +} diff --git a/Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs b/Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs index e0c7f13..f2ae6f6 100644 --- a/Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs +++ b/Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs @@ -1,38 +1,38 @@ -namespace Esolang.Generator; - -/// -/// Specifies the return type of the generated method. -/// -public enum MethodReturnKind -{ - /// The return type is invalid or unsupported. - Invalid, - /// The method returns void. - Void, - /// The method returns int. - Int32, - /// The method returns string. - String, - /// The method returns string (nullable). - NullableString, - /// The method returns Task. - Task, - /// The method returns Task<int>. - TaskInt32, - /// The method returns Task<string>. - TaskString, - /// The method returns Task<string?>. - TaskNullableString, - /// The method returns ValueTask. - ValueTask, - /// The method returns ValueTask<int>. - ValueTaskInt32, - /// The method returns ValueTask<string>. - ValueTaskString, - /// The method returns ValueTask<string?>. - ValueTaskNullableString, - /// The method returns IEnumerable<byte>. - IEnumerableByte, - /// The method returns IAsyncEnumerable<byte>. - IAsyncEnumerableByte, -} +namespace Esolang.Generator; + +/// +/// Specifies the return type of the generated method. +/// +public enum MethodReturnKind +{ + /// The return type is invalid or unsupported. + Invalid, + /// The method returns void. + Void, + /// The method returns int. + Int32, + /// The method returns string. + String, + /// The method returns string (nullable). + NullableString, + /// The method returns Task. + Task, + /// The method returns Task<int>. + TaskInt32, + /// The method returns Task<string>. + TaskString, + /// The method returns Task<string?>. + TaskNullableString, + /// The method returns ValueTask. + ValueTask, + /// The method returns ValueTask<int>. + ValueTaskInt32, + /// The method returns ValueTask<string>. + ValueTaskString, + /// The method returns ValueTask<string?>. + ValueTaskNullableString, + /// The method returns IEnumerable<byte>. + IEnumerableByte, + /// The method returns IAsyncEnumerable<byte>. + IAsyncEnumerableByte, +} diff --git a/Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs b/Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs index 2c2dd8a..3f88aec 100644 --- a/Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs +++ b/Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs @@ -1,228 +1,228 @@ -#nullable enable -using Microsoft.CodeAnalysis; -using System.Diagnostics.CodeAnalysis; -using static Esolang.Generator.BindingError; - -namespace Esolang.Generator; - -/// -/// Provides utility methods for binding method signatures to . -/// -[ExcludeFromCodeCoverage] -public static class MethodSignatureBinder -{ - /// - /// Binds the specified method symbol to a . - /// - /// The method symbol to bind. - /// The known types for the compilation. - /// The result of the binding. - public static MethodSignatureBinding Bind( - IMethodSymbol method, - KnownTypes types) - { - var returnKind = BindReturnKind(method.ReturnType, types); - if (returnKind == MethodReturnKind.Invalid) - { - return new MethodSignatureBinding(returnKind, MethodInputKind.None, MethodOutputKind.None, "", "", null, null, false, method.Parameters, new UnsupportedReturnType(method.ReturnType, method.Locations.FirstOrDefault())); - } - - var outputKind = BindDefaultOutputKind(returnKind); - var inputKind = MethodInputKind.None; - var inputExpr = ""; - var outputExpr = ""; - string? cancellationTokenName = null; - string? loggerExpression = null; - var isLoggerFromParameter = false; - var unhandledParameters = new List(); - - foreach (var p in method.Parameters) - { - if (p.RefKind != RefKind.None) - { - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new InvalidParameterModifier(p, p.Locations.FirstOrDefault())); - } - - if (types.IsString(p.Type, false)) - { - if (inputKind != MethodInputKind.None) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); - - inputKind = MethodInputKind.String; - inputExpr = p.Name; - continue; - } - - if (types.IsTextReader(p.Type)) - { - if (inputKind != MethodInputKind.None) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); - - inputKind = MethodInputKind.TextReader; - inputExpr = p.Name; - continue; - } - - if (types.IsPipeReader(p.Type)) - { - if (inputKind != MethodInputKind.None) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); - - inputKind = MethodInputKind.PipeReader; - inputExpr = p.Name; - continue; - } - - if (types.IsTextWriter(p.Type)) - { - if (IsOutputReturning(returnKind)) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new ReturnOutputConflict(p, p.Locations.FirstOrDefault())); - - if (outputKind != MethodOutputKind.None) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateOutput(p, outputKind, p.Locations.FirstOrDefault())); - - outputKind = MethodOutputKind.TextWriter; - outputExpr = p.Name; - continue; - } - - if (types.IsPipeWriter(p.Type)) - { - if (IsOutputReturning(returnKind)) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new ReturnOutputConflict(p, p.Locations.FirstOrDefault())); - - if (outputKind != MethodOutputKind.None) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateOutput(p, outputKind, p.Locations.FirstOrDefault())); - - outputKind = MethodOutputKind.PipeWriter; - outputExpr = p.Name; - continue; - } - - if (types.IsCancellationToken(p.Type)) - { - if (cancellationTokenName != null) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateCancellationToken(p, p.Locations.FirstOrDefault())); - - cancellationTokenName = p.Name; - continue; - } - - if (types.IsLogger(p.Type)) - { - if (loggerExpression != null) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateLogger(p, p.Locations.FirstOrDefault())); - - loggerExpression = p.Name; - isLoggerFromParameter = true; - continue; - } - - unhandledParameters.Add(p); - } - - loggerExpression ??= FindLoggerInContainingType(method.ContainingType, method.IsStatic, types, out isLoggerFromParameter); - - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, unhandledParameters); - } - - /// - /// Binds the return type symbol to a . - /// - public static MethodReturnKind BindReturnKind(ITypeSymbol returnType, KnownTypes types) - { - if (returnType.SpecialType == SpecialType.System_Void) return MethodReturnKind.Void; - if (returnType.SpecialType == SpecialType.System_Int32) return MethodReturnKind.Int32; - if (types.IsString(returnType, false)) return MethodReturnKind.String; - if (types.IsString(returnType, true)) return MethodReturnKind.NullableString; - if (types.IsTask(returnType)) return MethodReturnKind.Task; - if (types.IsTaskInt32(returnType)) return MethodReturnKind.TaskInt32; - if (types.IsTaskString(returnType, false)) return MethodReturnKind.TaskString; - if (types.IsTaskString(returnType, true)) return MethodReturnKind.TaskNullableString; - if (types.IsValueTask(returnType)) return MethodReturnKind.ValueTask; - if (types.IsValueTaskInt32(returnType)) return MethodReturnKind.ValueTaskInt32; - if (types.IsValueTaskString(returnType, false)) return MethodReturnKind.ValueTaskString; - if (types.IsValueTaskString(returnType, true)) return MethodReturnKind.ValueTaskNullableString; - if (types.IsIEnumerableByte(returnType)) return MethodReturnKind.IEnumerableByte; - if (types.IsIAsyncEnumerableByte(returnType)) return MethodReturnKind.IAsyncEnumerableByte; - - return MethodReturnKind.Invalid; - } - - /// - /// Gets the default output kind based on the return kind. - /// - static MethodOutputKind BindDefaultOutputKind(MethodReturnKind returnKind) => returnKind switch - { - MethodReturnKind.String or MethodReturnKind.NullableString or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString => MethodOutputKind.ReturnString, - MethodReturnKind.IEnumerableByte => MethodOutputKind.ReturnIEnumerable, - MethodReturnKind.IAsyncEnumerableByte => MethodOutputKind.ReturnIAsyncEnumerable, - _ => MethodOutputKind.None - }; - - /// - /// Gets a value indicating whether the return kind implies output is returned. - /// - static bool IsOutputReturning(MethodReturnKind returnKind) => returnKind switch - { - MethodReturnKind.String or MethodReturnKind.NullableString or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString or MethodReturnKind.IEnumerableByte or MethodReturnKind.IAsyncEnumerableByte => true, - _ => false - }; - - /// - /// Searches for a logger in the containing type (fields or constructor parameters). - /// - /// The type to search in. - /// Whether the target method is static. - /// The known types for the compilation. - /// Output: Whether the logger was found in a constructor parameter. - /// The expression to access the logger, or null if not found. - static string? FindLoggerInContainingType(ITypeSymbol? type, bool isStatic, KnownTypes types, out bool isFromParameter) - { - isFromParameter = false; - var currentType = type; - var shadowedNames = new HashSet(StringComparer.Ordinal); - var isBaseType = false; - - while (currentType != null) - { - foreach (var field in currentType.GetMembers().OfType()) - { - if (isStatic && !field.IsStatic) continue; - - // If searching in a base type, the field must be accessible (protected or public) - if (isBaseType && field.DeclaredAccessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal or Accessibility.Public or Accessibility.Internal)) - continue; - - if (types.IsLogger(field.Type)) - { - return field.Name; - } - - if (field.CanBeReferencedByName) - { - shadowedNames.Add(field.Name); - } - } - currentType = currentType.BaseType; - isBaseType = true; - } - - if (type is INamedTypeSymbol namedType) - { - foreach (var constructor in namedType.InstanceConstructors) - { - foreach (var parameter in constructor.Parameters) - { - if (types.IsLogger(parameter.Type) && !shadowedNames.Contains(parameter.Name)) - { - isFromParameter = true; - return parameter.Name; - } - } - } - } - - return null; - } -} +#nullable enable +using Microsoft.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using static Esolang.Generator.BindingError; + +namespace Esolang.Generator; + +/// +/// Provides utility methods for binding method signatures to . +/// +[ExcludeFromCodeCoverage] +public static class MethodSignatureBinder +{ + /// + /// Binds the specified method symbol to a . + /// + /// The method symbol to bind. + /// The known types for the compilation. + /// The result of the binding. + public static MethodSignatureBinding Bind( + IMethodSymbol method, + KnownTypes types) + { + var returnKind = BindReturnKind(method.ReturnType, types); + if (returnKind == MethodReturnKind.Invalid) + { + return new MethodSignatureBinding(returnKind, MethodInputKind.None, MethodOutputKind.None, "", "", null, null, false, method.Parameters, new UnsupportedReturnType(method.ReturnType, method.Locations.FirstOrDefault())); + } + + var outputKind = BindDefaultOutputKind(returnKind); + var inputKind = MethodInputKind.None; + var inputExpr = ""; + var outputExpr = ""; + string? cancellationTokenName = null; + string? loggerExpression = null; + var isLoggerFromParameter = false; + var unhandledParameters = new List(); + + foreach (var p in method.Parameters) + { + if (p.RefKind != RefKind.None) + { + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new InvalidParameterModifier(p, p.Locations.FirstOrDefault())); + } + + if (types.IsString(p.Type, false)) + { + if (inputKind != MethodInputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); + + inputKind = MethodInputKind.String; + inputExpr = p.Name; + continue; + } + + if (types.IsTextReader(p.Type)) + { + if (inputKind != MethodInputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); + + inputKind = MethodInputKind.TextReader; + inputExpr = p.Name; + continue; + } + + if (types.IsPipeReader(p.Type)) + { + if (inputKind != MethodInputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); + + inputKind = MethodInputKind.PipeReader; + inputExpr = p.Name; + continue; + } + + if (types.IsTextWriter(p.Type)) + { + if (IsOutputReturning(returnKind)) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new ReturnOutputConflict(p, p.Locations.FirstOrDefault())); + + if (outputKind != MethodOutputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateOutput(p, outputKind, p.Locations.FirstOrDefault())); + + outputKind = MethodOutputKind.TextWriter; + outputExpr = p.Name; + continue; + } + + if (types.IsPipeWriter(p.Type)) + { + if (IsOutputReturning(returnKind)) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new ReturnOutputConflict(p, p.Locations.FirstOrDefault())); + + if (outputKind != MethodOutputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateOutput(p, outputKind, p.Locations.FirstOrDefault())); + + outputKind = MethodOutputKind.PipeWriter; + outputExpr = p.Name; + continue; + } + + if (types.IsCancellationToken(p.Type)) + { + if (cancellationTokenName != null) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateCancellationToken(p, p.Locations.FirstOrDefault())); + + cancellationTokenName = p.Name; + continue; + } + + if (types.IsLogger(p.Type)) + { + if (loggerExpression != null) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateLogger(p, p.Locations.FirstOrDefault())); + + loggerExpression = p.Name; + isLoggerFromParameter = true; + continue; + } + + unhandledParameters.Add(p); + } + + loggerExpression ??= FindLoggerInContainingType(method.ContainingType, method.IsStatic, types, out isLoggerFromParameter); + + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, unhandledParameters); + } + + /// + /// Binds the return type symbol to a . + /// + public static MethodReturnKind BindReturnKind(ITypeSymbol returnType, KnownTypes types) + { + if (returnType.SpecialType == SpecialType.System_Void) return MethodReturnKind.Void; + if (returnType.SpecialType == SpecialType.System_Int32) return MethodReturnKind.Int32; + if (types.IsString(returnType, false)) return MethodReturnKind.String; + if (types.IsString(returnType, true)) return MethodReturnKind.NullableString; + if (types.IsTask(returnType)) return MethodReturnKind.Task; + if (types.IsTaskInt32(returnType)) return MethodReturnKind.TaskInt32; + if (types.IsTaskString(returnType, false)) return MethodReturnKind.TaskString; + if (types.IsTaskString(returnType, true)) return MethodReturnKind.TaskNullableString; + if (types.IsValueTask(returnType)) return MethodReturnKind.ValueTask; + if (types.IsValueTaskInt32(returnType)) return MethodReturnKind.ValueTaskInt32; + if (types.IsValueTaskString(returnType, false)) return MethodReturnKind.ValueTaskString; + if (types.IsValueTaskString(returnType, true)) return MethodReturnKind.ValueTaskNullableString; + if (types.IsIEnumerableByte(returnType)) return MethodReturnKind.IEnumerableByte; + if (types.IsIAsyncEnumerableByte(returnType)) return MethodReturnKind.IAsyncEnumerableByte; + + return MethodReturnKind.Invalid; + } + + /// + /// Gets the default output kind based on the return kind. + /// + static MethodOutputKind BindDefaultOutputKind(MethodReturnKind returnKind) => returnKind switch + { + MethodReturnKind.String or MethodReturnKind.NullableString or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString => MethodOutputKind.ReturnString, + MethodReturnKind.IEnumerableByte => MethodOutputKind.ReturnIEnumerable, + MethodReturnKind.IAsyncEnumerableByte => MethodOutputKind.ReturnIAsyncEnumerable, + _ => MethodOutputKind.None + }; + + /// + /// Gets a value indicating whether the return kind implies output is returned. + /// + static bool IsOutputReturning(MethodReturnKind returnKind) => returnKind switch + { + MethodReturnKind.String or MethodReturnKind.NullableString or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString or MethodReturnKind.IEnumerableByte or MethodReturnKind.IAsyncEnumerableByte => true, + _ => false + }; + + /// + /// Searches for a logger in the containing type (fields or constructor parameters). + /// + /// The type to search in. + /// Whether the target method is static. + /// The known types for the compilation. + /// Output: Whether the logger was found in a constructor parameter. + /// The expression to access the logger, or null if not found. + static string? FindLoggerInContainingType(ITypeSymbol? type, bool isStatic, KnownTypes types, out bool isFromParameter) + { + isFromParameter = false; + var currentType = type; + var shadowedNames = new HashSet(StringComparer.Ordinal); + var isBaseType = false; + + while (currentType != null) + { + foreach (var field in currentType.GetMembers().OfType()) + { + if (isStatic && !field.IsStatic) continue; + + // If searching in a base type, the field must be accessible (protected or public) + if (isBaseType && field.DeclaredAccessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal or Accessibility.Public or Accessibility.Internal)) + continue; + + if (types.IsLogger(field.Type)) + { + return field.Name; + } + + if (field.CanBeReferencedByName) + { + shadowedNames.Add(field.Name); + } + } + currentType = currentType.BaseType; + isBaseType = true; + } + + if (type is INamedTypeSymbol namedType) + { + foreach (var constructor in namedType.InstanceConstructors) + { + foreach (var parameter in constructor.Parameters) + { + if (types.IsLogger(parameter.Type) && !shadowedNames.Contains(parameter.Name)) + { + isFromParameter = true; + return parameter.Name; + } + } + } + } + + return null; + } +} diff --git a/Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs b/Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs index da70428..b5b93c0 100644 --- a/Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs +++ b/Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs @@ -1,98 +1,98 @@ -#nullable enable -using Microsoft.CodeAnalysis; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace Esolang.Generator; - -/// -/// Represents the result of binding a method signature for generation. -/// -/// The return kind of the method. -/// The input kind of the method. -/// The output kind of the method. -/// The expression to access the input (e.g., parameter name). -/// The expression to access the output (e.g., parameter name). -/// The name of the cancellation token parameter, if any. -/// The expression to access the logger (e.g., "loggerParam", "this._logger"). -/// Whether the logger is obtained from a method parameter. -/// Parameters that were not handled by the common binding logic. -/// The diagnostic error if the binding failed. -[DebuggerDisplay("{ToString(),nq}")] -[ExcludeFromCodeCoverage] -public record struct MethodSignatureBinding( - MethodReturnKind ReturnKind, - MethodInputKind InputKind, - MethodOutputKind OutputKind, - string InputExpression, - string OutputExpression, - string? CancellationTokenName, - string? LoggerExpression, - bool IsLoggerFromParameter, - IReadOnlyList UnhandledParameters, - BindingError? Error = null) -{ - /// Whether the binding is successful. - [MemberNotNullWhen(false, nameof(Error))] - public readonly bool IsValid => Error is null; - - /// Gets a value indicating whether the method has an explicit input mechanism. - public readonly bool HasExplicitInput => InputKind != MethodInputKind.None; - - /// Gets a value indicating whether the method has an explicit output mechanism. - public readonly bool HasExplicitOutput => OutputKind != MethodOutputKind.None; - - /// Gets a value indicating whether the method is asynchronous. - public readonly bool IsAsync => ReturnKind switch - { - MethodReturnKind.Task or MethodReturnKind.TaskInt32 or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or - MethodReturnKind.ValueTask or MethodReturnKind.ValueTaskInt32 or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString or - MethodReturnKind.IAsyncEnumerableByte => true, - _ => false - }; - - /// Gets a value indicating whether the method returns an enumerable. - public readonly bool IsEnumerable => ReturnKind == MethodReturnKind.IEnumerableByte; - - /// Gets a value indicating whether the method returns an async enumerable. - public readonly bool IsAsyncEnumerable => ReturnKind == MethodReturnKind.IAsyncEnumerableByte; - - [ExcludeFromCodeCoverage] - readonly bool PrintMembers(StringBuilder builder) - { - builder.Append(nameof(IsValid)).Append('=').Append(IsValid).Append(", "); - builder.Append(nameof(ReturnKind)).Append('=').Append(ReturnKind).Append(", "); - builder.Append(nameof(InputKind)).Append('=').Append(InputKind).Append(", "); - builder.Append(nameof(OutputKind)).Append('=').Append(OutputKind).Append(", "); - builder.Append(nameof(InputExpression)).Append('=').Append(InputExpression).Append(", "); - builder.Append(nameof(OutputExpression)).Append('=').Append(OutputExpression).Append(", "); - builder.Append(nameof(CancellationTokenName)).Append('=').Append(CancellationTokenName).Append(", "); - builder.Append(nameof(LoggerExpression)).Append('=').Append(LoggerExpression).Append(", "); - builder.Append(nameof(IsLoggerFromParameter)).Append('=').Append(IsLoggerFromParameter).Append(", "); - builder.Append(nameof(UnhandledParameters)).Append("=["); - for (var i = 0; i < UnhandledParameters.Count; i++) - { - if (i > 0) builder.Append(", "); - builder.Append(UnhandledParameters[i].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - } - builder.Append("], "); - builder.Append(nameof(Error)).Append('=').Append(Error); - return true; - } - - /// - [ExcludeFromCodeCoverage] - public override readonly string ToString() - { - var builder = new StringBuilder(); - builder.Append(nameof(MethodSignatureBinding)).Append(" {"); - if (!PrintMembers(builder)) - { - builder.Append(' '); - } - builder.Append('}'); - return builder.ToString(); - } - -} +#nullable enable +using Microsoft.CodeAnalysis; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Esolang.Generator; + +/// +/// Represents the result of binding a method signature for generation. +/// +/// The return kind of the method. +/// The input kind of the method. +/// The output kind of the method. +/// The expression to access the input (e.g., parameter name). +/// The expression to access the output (e.g., parameter name). +/// The name of the cancellation token parameter, if any. +/// The expression to access the logger (e.g., "loggerParam", "this._logger"). +/// Whether the logger is obtained from a method parameter. +/// Parameters that were not handled by the common binding logic. +/// The diagnostic error if the binding failed. +[DebuggerDisplay("{ToString(),nq}")] +[ExcludeFromCodeCoverage] +public record struct MethodSignatureBinding( + MethodReturnKind ReturnKind, + MethodInputKind InputKind, + MethodOutputKind OutputKind, + string InputExpression, + string OutputExpression, + string? CancellationTokenName, + string? LoggerExpression, + bool IsLoggerFromParameter, + IReadOnlyList UnhandledParameters, + BindingError? Error = null) +{ + /// Whether the binding is successful. + [MemberNotNullWhen(false, nameof(Error))] + public readonly bool IsValid => Error is null; + + /// Gets a value indicating whether the method has an explicit input mechanism. + public readonly bool HasExplicitInput => InputKind != MethodInputKind.None; + + /// Gets a value indicating whether the method has an explicit output mechanism. + public readonly bool HasExplicitOutput => OutputKind != MethodOutputKind.None; + + /// Gets a value indicating whether the method is asynchronous. + public readonly bool IsAsync => ReturnKind switch + { + MethodReturnKind.Task or MethodReturnKind.TaskInt32 or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or + MethodReturnKind.ValueTask or MethodReturnKind.ValueTaskInt32 or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString or + MethodReturnKind.IAsyncEnumerableByte => true, + _ => false + }; + + /// Gets a value indicating whether the method returns an enumerable. + public readonly bool IsEnumerable => ReturnKind == MethodReturnKind.IEnumerableByte; + + /// Gets a value indicating whether the method returns an async enumerable. + public readonly bool IsAsyncEnumerable => ReturnKind == MethodReturnKind.IAsyncEnumerableByte; + + [ExcludeFromCodeCoverage] + readonly bool PrintMembers(StringBuilder builder) + { + builder.Append(nameof(IsValid)).Append('=').Append(IsValid).Append(", "); + builder.Append(nameof(ReturnKind)).Append('=').Append(ReturnKind).Append(", "); + builder.Append(nameof(InputKind)).Append('=').Append(InputKind).Append(", "); + builder.Append(nameof(OutputKind)).Append('=').Append(OutputKind).Append(", "); + builder.Append(nameof(InputExpression)).Append('=').Append(InputExpression).Append(", "); + builder.Append(nameof(OutputExpression)).Append('=').Append(OutputExpression).Append(", "); + builder.Append(nameof(CancellationTokenName)).Append('=').Append(CancellationTokenName).Append(", "); + builder.Append(nameof(LoggerExpression)).Append('=').Append(LoggerExpression).Append(", "); + builder.Append(nameof(IsLoggerFromParameter)).Append('=').Append(IsLoggerFromParameter).Append(", "); + builder.Append(nameof(UnhandledParameters)).Append("=["); + for (var i = 0; i < UnhandledParameters.Count; i++) + { + if (i > 0) builder.Append(", "); + builder.Append(UnhandledParameters[i].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + builder.Append("], "); + builder.Append(nameof(Error)).Append('=').Append(Error); + return true; + } + + /// + [ExcludeFromCodeCoverage] + public override readonly string ToString() + { + var builder = new StringBuilder(); + builder.Append(nameof(MethodSignatureBinding)).Append(" {"); + if (!PrintMembers(builder)) + { + builder.Append(' '); + } + builder.Append('}'); + return builder.ToString(); + } + +} diff --git a/Interpreter/Esolang.Interpreter.Abstractions/.editorconfig b/Interpreter/Esolang.Interpreter.Abstractions/.editorconfig new file mode 100644 index 0000000..50c37f6 --- /dev/null +++ b/Interpreter/Esolang.Interpreter.Abstractions/.editorconfig @@ -0,0 +1,2 @@ +[*] +generated_code = true diff --git a/Interpreter/Esolang.Interpreter.Abstractions/InterpreterExtensions.cs b/Interpreter/Esolang.Interpreter.Abstractions/InterpreterExtensions.cs new file mode 100644 index 0000000..9501c3a --- /dev/null +++ b/Interpreter/Esolang.Interpreter.Abstractions/InterpreterExtensions.cs @@ -0,0 +1,59 @@ +#nullable enable +using Esolang.Processor; +using System.Diagnostics.CodeAnalysis; +using static Esolang.Processor.IOEvent; + +namespace Esolang.Interpreter; + +/// +/// Provides extension methods for running in an interpreter context. +/// +[ExcludeFromCodeCoverage] +public static class InterpreterExtensions +{ + /// + /// Executes the processor using standard I/O (Console.In, Console.Out). + /// + /// The event processor. + /// The cancellation token. + /// The exit code. + public static async ValueTask RunToConsoleAsync( + this IEventProcessor processor, + CancellationToken cancellationToken = default) + { + await foreach (var ioEvent in processor.RunAsyncEnumerable(cancellationToken)) + { + switch (ioEvent) + { + case InputCharEvent charInput: + charInput.Write(await ReadCharFromConsoleAsync(cancellationToken)); + break; + case InputIntEvent intInput: + var line = await ReadLineFromConsoleAsync(cancellationToken); + if (int.TryParse(line, out var i)) + { + intInput.Write(i); + } + break; + case OutputCharEvent charOutput: + Console.Write(charOutput.Output); + break; + case OutputIntEvent intOutput: + Console.Write(intOutput.Output); + break; + case EndEvent end: + return end.ExitCode; + } + } + return 0; + } + + static async ValueTask ReadCharFromConsoleAsync(CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + var c = Console.In.Read(); + return c == -1 ? '\0' : (char)c; + } + + static async ValueTask ReadLineFromConsoleAsync(CancellationToken ct) => await Console.In.ReadLineAsync(ct); +} diff --git a/Interpreter/FungeInterpreterExtensions.cs b/Interpreter/FungeInterpreterExtensions.cs index 10d4fb1..892b3e6 100644 --- a/Interpreter/FungeInterpreterExtensions.cs +++ b/Interpreter/FungeInterpreterExtensions.cs @@ -1,6 +1,6 @@ using Esolang.Funge.Parser; using Esolang.Funge.Processor; -using Esolang.Processor; +using Esolang.Interpreter; using System.Collections; using System.CommandLine; @@ -38,35 +38,7 @@ public static RootCommand BuildRootCommand() commandLineArguments: [path], environmentVariables: env); - var exitCode = 0; - await foreach (var ioEvent in proc.RunAsyncEnumerable(cancellationToken)) - { - switch (ioEvent) - { - case InputCharEvent ice: - { - var c = Console.In.Read(); - if (c != -1) ice.Write((char)c); - } - break; - case InputIntEvent iie: - { - var line = await Console.In.ReadLineAsync(cancellationToken).ConfigureAwait(false); - if (int.TryParse(line, out var val)) iie.Write(val); - } - break; - case OutputCharEvent oce: - Console.Out.Write(oce.Output); - break; - case OutputIntEvent oie: - Console.Out.Write(oie.Output); - break; - case EndEvent ee: - exitCode = ee.ExitCode; - break; - } - } - return exitCode; + return await proc.RunToConsoleAsync(cancellationToken); }); return rootCommand; diff --git a/Processor.Tests/FungeProcessorTests.cs b/Processor.Tests/FungeProcessorTests.cs index c1c6c23..c57c159 100644 --- a/Processor.Tests/FungeProcessorTests.cs +++ b/Processor.Tests/FungeProcessorTests.cs @@ -1,4 +1,4 @@ -using Esolang.Processor; +using static Esolang.Processor.IOEvent; namespace Esolang.Funge.Processor.Tests; diff --git a/Processor/Esolang.Processor.Abstractions/IOEvent.cs b/Processor/Esolang.Processor.Abstractions/IOEvent.cs new file mode 100644 index 0000000..d9124cc --- /dev/null +++ b/Processor/Esolang.Processor.Abstractions/IOEvent.cs @@ -0,0 +1,110 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; + +namespace Esolang.Processor; + +/// +/// Represents an I/O event. +/// +[ExcludeFromCodeCoverage] +public abstract class IOEvent +{ + IOEvent() { } + + /// + /// Creates an event requesting a character input. + /// + /// The action to write the input character to the processor. + /// An event requesting a character input. + public static InputCharEvent InputChar(Action write) => new(write); + + /// + /// Creates an event requesting an integer input. + /// + /// The action to write the input integer to the processor. + /// An event requesting an integer input. + public static InputIntEvent InputInt(Action write) => new(write); + + /// + /// Creates an event that outputs a character. + /// + /// The character to output. + /// An event that outputs a character. + public static OutputCharEvent OutputChar(char output) => new(output); + + /// + /// Creates an event that outputs an integer. + /// + /// The integer to output. + /// An event that outputs an integer. + public static OutputIntEvent OutputInt(int output) => new(output); + + /// + /// Creates an event indicating the end of execution. + /// + /// The exit code. + /// An event indicating the end of execution. + public static EndEvent End(int exitCode) => new(exitCode); + + /// + /// Represents an event requesting a character input. + /// + public sealed class InputCharEvent(Action write) : IOEvent + { + /// + /// Writes the input character to the processor. + /// + /// The input character. + public void Write(char c) => write(c); + } + + /// + /// Represents an event requesting an integer input. + /// + /// The action to write the input integer to the processor. + public sealed class InputIntEvent(Action write) : IOEvent + { + /// + /// Writes the input integer to the processor. + /// + /// The input integer. + public void Write(int i) => write(i); + } + + /// + /// Represents an event that outputs a character. + /// + /// The character to output. + public sealed class OutputCharEvent(char Output) : IOEvent + { + /// + /// The character to output. + /// + public char Output { get; } = Output; + } + + /// + /// Represents an event that outputs an integer. + /// + /// The integer to output. + public sealed class OutputIntEvent(int Output) : IOEvent + { + /// + /// The integer to output. + /// + public int Output { get; } = Output; + } + + /// + /// Represents an event indicating the end of execution. + /// + /// The exit code. + public sealed class EndEvent(int exitCode) : IOEvent + { + /// + /// The exit code. + /// + public int ExitCode { get; } = exitCode; + } + +} diff --git a/Processor/Esolang.Processor.Abstractions/IProcessor.cs b/Processor/Esolang.Processor.Abstractions/IProcessor.cs index b4becc2..f93bdb2 100644 --- a/Processor/Esolang.Processor.Abstractions/IProcessor.cs +++ b/Processor/Esolang.Processor.Abstractions/IProcessor.cs @@ -1,106 +1,32 @@ -#nullable enable -using System.Diagnostics.CodeAnalysis; - -namespace Esolang.Processor; - -/// -/// Common base interface for all processors. -/// -public interface IProcessor { } - -/// -/// Common base interface for processors that hold a program to be executed. -/// -/// The type of the parsed program. -public interface IProcessor : IProcessor -{ - /// The parsed program. - TProgram Program { get; } -} - -/// -/// Execution model based on a stream of I/O events. -/// -public interface IEventProcessor : IProcessor -{ - /// - /// Executes the processor and returns a stream of I/O events. - /// - /// The cancellation token. - /// An asynchronous stream of I/O events. - IAsyncEnumerable RunAsyncEnumerable( - CancellationToken cancellationToken = default); -} - -/// -/// Represents an I/O event. -/// -public interface IOEvent -{ - -} - -/// -/// Represents an event requesting a character input. -/// -[ExcludeFromCodeCoverage] -public abstract class InputCharEvent : IOEvent -{ - /// - /// Writes the input character to the processor. - /// - /// The input character. - public abstract void Write(char c); -} - -/// -/// Represents an event requesting an integer input. -/// -[ExcludeFromCodeCoverage] -public abstract class InputIntEvent : IOEvent -{ - /// - /// Writes the input integer to the processor. - /// - /// The input integer. - public abstract void Write(int i); -} - -/// -/// Represents an event that outputs a character. -/// -/// The character to output. -[ExcludeFromCodeCoverage] -public sealed class OutputCharEvent(char Output) : IOEvent -{ - /// - /// The character to output. - /// - public char Output { get; } = Output; -} - -/// -/// Represents an event that outputs an integer. -/// -/// The integer to output. -[ExcludeFromCodeCoverage] -public sealed class OutputIntEvent(int Output) : IOEvent -{ - /// - /// The integer to output. - /// - public int Output { get; } = Output; -} - -/// -/// Represents an event indicating the end of execution. -/// -/// The exit code. -[ExcludeFromCodeCoverage] -public sealed class EndEvent(int exitCode) : IOEvent -{ - /// - /// The exit code. - /// - public int ExitCode { get; } = exitCode; -} +#nullable enable + +namespace Esolang.Processor; + +/// +/// Common base interface for all processors. +/// +public interface IProcessor { } + +/// +/// Common base interface for processors that hold a program to be executed. +/// +/// The type of the parsed program. +public interface IProcessor : IProcessor +{ + /// The parsed program. + TProgram Program { get; } +} + +/// +/// Execution model based on a stream of I/O events. +/// +public interface IEventProcessor : IProcessor +{ + /// + /// Executes the processor and returns a stream of I/O events. + /// + /// The cancellation token. + /// An asynchronous stream of I/O events. + IAsyncEnumerable RunAsyncEnumerable( + CancellationToken cancellationToken = default); +} diff --git a/Processor/Esolang.Processor.Abstractions/PipeProcessorExtensions.cs b/Processor/Esolang.Processor.Abstractions/PipeProcessorExtensions.cs deleted file mode 100644 index 2d8f85e..0000000 --- a/Processor/Esolang.Processor.Abstractions/PipeProcessorExtensions.cs +++ /dev/null @@ -1,112 +0,0 @@ -#nullable enable -using System.Buffers; -using System.Diagnostics.CodeAnalysis; -using System.IO.Pipelines; -using System.Text; - -namespace Esolang.Processor; - -/// -/// Provides extension methods for running using and . -/// -[ExcludeFromCodeCoverage] -public static class PipeProcessorExtensions -{ - /// - /// Executes the processor until it reaches an . - /// - /// The event processor. - /// The input pipe reader. - /// The output pipe writer. - /// The cancellation token. - /// The exit code. - /// Thrown when input or output is null depending on the event. - public static async ValueTask RunToEndAsync( - this IEventProcessor processor, - PipeReader? input, - PipeWriter? output, - CancellationToken cancellationToken = default) - { - await foreach (var ev in processor.RunAsyncEnumerable(cancellationToken)) - { - switch (ev) - { - case InputCharEvent inputChar: - if (input == null) - throw new ArgumentNullException(nameof(input)); - var result = await input.ReadAtLeastAsync(1, cancellationToken); - var buffer = ArrayPool.Shared.Rent(1); - try - { -#if NETSTANDARD2_1_OR_GREATER - result.Buffer.Slice(0, 1).CopyTo(buffer.AsSpan()); -#else - result.Buffer.Slice(0, 1).ToArray().CopyTo(buffer, 0); -#endif - input.AdvanceTo(result.Buffer.GetPosition(1)); - inputChar.Write((char)buffer[0]); - } - finally - { - ArrayPool.Shared.Return(buffer); - } - break; - case InputIntEvent inputInt: - if (input == null) - throw new ArgumentNullException(nameof(input)); - var result2 = await input.ReadAtLeastAsync(1, cancellationToken); - var buffer2 = ArrayPool.Shared.Rent(1); - try - { -#if NETSTANDARD2_1_OR_GREATER - result2.Buffer.Slice(0, 4).CopyTo(buffer2.AsSpan()); -#else - result2.Buffer.Slice(0, 4).ToArray().CopyTo(buffer2, 0); -#endif - input.AdvanceTo(result2.Buffer.GetPosition(4)); - inputInt.Write(BitConverter.ToInt32(buffer2, 0)); - } - finally - { - ArrayPool.Shared.Return(buffer2); - } - break; - case OutputCharEvent outputChar: - if (output == null) - throw new ArgumentNullException(nameof(output)); - output.Write(Encoding.UTF8.GetBytes([outputChar.Output])); - await output.FlushAsync(cancellationToken); - break; - case OutputIntEvent outputInt: - if (output == null) - throw new ArgumentNullException(nameof(output)); - output.Write(Encoding.UTF8.GetBytes(outputInt.Output.ToString())); - await output.FlushAsync(cancellationToken); - break; - case EndEvent end: - return end.ExitCode; - } - } - return 0; - } - - /// - /// Executes the processor synchronously until it reaches an . - /// - /// The event processor. - /// The input pipe reader. - /// The output pipe writer. - /// The cancellation token. - /// The exit code. - public static int RunToEnd( - this IEventProcessor processor, - PipeReader? input = null, - PipeWriter? output = null, - CancellationToken cancellationToken = default) - { - var result = RunToEndAsync(processor, input, output, cancellationToken); - if (result.IsCompleted) - return result.GetAwaiter().GetResult(); - return result.AsTask().GetAwaiter().GetResult(); - } -} diff --git a/Processor/Esolang.Processor.Abstractions/TextProcessorExtensions.cs b/Processor/Esolang.Processor.Abstractions/TextProcessorExtensions.cs deleted file mode 100644 index d2b6064..0000000 --- a/Processor/Esolang.Processor.Abstractions/TextProcessorExtensions.cs +++ /dev/null @@ -1,112 +0,0 @@ -#nullable enable -using System.Buffers; -using System.Diagnostics.CodeAnalysis; - -namespace Esolang.Processor; - -/// -/// Provides extension methods for running using and . -/// -[ExcludeFromCodeCoverage] -public static class TextProcessorExtensions -{ - /// - /// Executes the processor until it reaches an . - /// - /// The event processor. - /// The input text reader. - /// The output text writer. - /// The cancellation token. - /// The exit code. - /// Thrown when input or output is null depending on the event. - public static async ValueTask RunToEndAsync( - this IEventProcessor processor, - TextReader? input = null, - TextWriter? output = null, - CancellationToken cancellationToken = default) - { - await foreach (var ioEvent in processor.RunAsyncEnumerable(cancellationToken)) - { - switch (ioEvent) - { - case InputCharEvent charInput: - if (input is null) - throw new ArgumentNullException(nameof(input)); - { - var buffer = ArrayPool.Shared.Rent(1); - try - { - int read; - do - { -#if NETSTANDARD2_1_OR_GREATER - read = await input.ReadAsync(buffer.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); -#else - read = await input.ReadAsync(buffer, 0, 1).ConfigureAwait(false); -#endif - if (read < 0) continue; - charInput.Write(buffer[0]); - break; - } while (read < 0 && !cancellationToken.IsCancellationRequested); - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - break; - case InputIntEvent intInput: - if (input is null) - throw new ArgumentNullException(nameof(input)); - { - var inputString = await input.ReadLineAsync(); - if (int.TryParse(inputString, out var i)) - { - intInput.Write(i); - } - } - break; - case OutputCharEvent charOutput: - if (output is null) - throw new ArgumentNullException(nameof(output)); - { - await output.WriteAsync(charOutput.Output).ConfigureAwait(false); - await output.FlushAsync().ConfigureAwait(false); - } - break; - case OutputIntEvent intOutput: - if (output is null) - throw new ArgumentNullException(nameof(output)); - { - await output.WriteLineAsync(intOutput.Output.ToString()).ConfigureAwait(false); - await output.FlushAsync().ConfigureAwait(false); - } - break; - case EndEvent endEvent: - return endEvent.ExitCode; - } - } - return 0; - } - - /// - /// Executes the processor synchronously until it reaches an . - /// - /// The event processor. - /// The input text reader. - /// The output text writer. - /// The cancellation token. - /// The exit code. - [Obsolete("Use RunToEndAsync instead.")] - public static int RunToEnd( - this IEventProcessor processor, - TextReader? input = null, - TextWriter? output = null, - CancellationToken cancellationToken = default) - { - var result = RunToEndAsync(processor, input, output, cancellationToken); - if (result.IsCompleted) - return result.GetAwaiter().GetResult(); - return result.AsTask().GetAwaiter().GetResult(); - } -} diff --git a/Processor/FungeProcessor.IEventProcessor.cs b/Processor/FungeProcessor.IEventProcessor.cs index baa3566..1184fa4 100644 --- a/Processor/FungeProcessor.IEventProcessor.cs +++ b/Processor/FungeProcessor.IEventProcessor.cs @@ -1,21 +1,11 @@ using Esolang.Processor; using System.Runtime.CompilerServices; +using static Esolang.Processor.IOEvent; namespace Esolang.Funge.Processor; public sealed partial class FungeProcessor : IEventProcessor { - sealed class FungeInputCharEvent : InputCharEvent - { - public int? Value { get; private set; } - public override void Write(char c) => Value = c; - } - - sealed class FungeInputIntEvent : InputIntEvent - { - public int? Value { get; private set; } - public override void Write(int i) => Value = i; - } sealed class FungeState { diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index aa1ef71..7d70501 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -2,6 +2,7 @@ using Esolang.Processor; using System.Collections; using System.Diagnostics; +using static Esolang.Processor.IOEvent; namespace Esolang.Funge.Processor; @@ -296,28 +297,30 @@ IEnumerable ExecuteInstruction( // ── I/O ────────────────────────────────────────────────────────── case '.': // Output Integer - yield return new OutputIntEvent(ip.StackStack.Pop()); - yield return new OutputCharEvent(' '); + yield return OutputInt(ip.StackStack.Pop()); + yield return OutputChar(' '); break; case ',': // Output Character - yield return new OutputCharEvent((char)ip.StackStack.Pop()); + yield return OutputChar((char)ip.StackStack.Pop()); break; case '&': // Input Integer { - var ev = new FungeInputIntEvent(); + int? input = null; + var ev = InputInt(value => input = value); yield return ev; - if (ev.Value.HasValue) ip.StackStack.Push(ev.Value.Value); + if (input.HasValue) ip.StackStack.Push(input.Value); else ip.Delta = ip.Delta.Reflect(); break; } case '~': // Input Character { - var ev = new FungeInputCharEvent(); + char? input = null; + var ev = InputChar(value => input = value); yield return ev; - if (ev.Value.HasValue) ip.StackStack.Push(ev.Value.Value); + if (input.HasValue) ip.StackStack.Push(input.Value); else ip.Delta = ip.Delta.Reflect(); break; } From 1f3b33b814dfc8805926efc33bbd32b7d9f163f5 Mon Sep 17 00:00:00 2001 From: juner Date: Wed, 3 Jun 2026 21:51:52 +0900 Subject: [PATCH 19/25] Refactor: Remove obsolete method input/output/return kind enums and signature binder - Deleted `MethodInputKind`, `MethodOutputKind`, and `MethodReturnKind` enums as they are no longer needed. - Removed `MethodSignatureBinder` and `MethodSignatureBinding` classes, which were responsible for binding method signatures. - Updated project files to reflect changes in target frameworks and dependencies. - Cleaned up unused files and configurations across the project. --- .editorconfig | 1 - .github/workflows/aot.yml | 61 +++++ .github/workflows/dotnet.yml | 162 +++++++----- Directory.Build.props | 6 +- Directory.Build.targets | 4 +- Generator/Esolang.Funge.Generator.csproj | 3 + .../.editorconfig | 2 - .../BindingError.cs | 110 -------- .../IsExternalInit.cs | 11 - .../KnownTypes.cs | 250 ------------------ .../MethodInputKind.cs | 17 -- .../MethodOutputKind.cs | 20 -- .../MethodReturnKind.cs | 38 --- .../MethodSignatureBinder.cs | 228 ---------------- .../MethodSignatureBinding.cs | 98 ------- .../Esolang.Funge.Generator.targets | 6 + .../Esolang.Funge.Interpreter.Tests.csproj | 2 +- Interpreter/Esolang.Funge.Interpreter.csproj | 15 +- .../.editorconfig | 2 - .../InterpreterExtensions.cs | 59 ----- Parser/Esolang.Funge.Parser.csproj | 7 - Processor/Esolang.Funge.Processor.csproj | 11 +- .../.editorconfig | 2 - .../Esolang.Processor.Abstractions/IOEvent.cs | 110 -------- .../IProcessor.cs | 32 --- coverlet.collect.runsettings | 12 - .../Esolang.Funge.Generator.UseConsole.csproj | 9 + 27 files changed, 192 insertions(+), 1086 deletions(-) create mode 100644 .github/workflows/aot.yml delete mode 100644 Generator/Esolang.Generator.Abstractions/.editorconfig delete mode 100644 Generator/Esolang.Generator.Abstractions/BindingError.cs delete mode 100644 Generator/Esolang.Generator.Abstractions/IsExternalInit.cs delete mode 100644 Generator/Esolang.Generator.Abstractions/KnownTypes.cs delete mode 100644 Generator/Esolang.Generator.Abstractions/MethodInputKind.cs delete mode 100644 Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs delete mode 100644 Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs delete mode 100644 Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs delete mode 100644 Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs delete mode 100644 Interpreter/Esolang.Interpreter.Abstractions/.editorconfig delete mode 100644 Interpreter/Esolang.Interpreter.Abstractions/InterpreterExtensions.cs delete mode 100644 Processor/Esolang.Processor.Abstractions/.editorconfig delete mode 100644 Processor/Esolang.Processor.Abstractions/IOEvent.cs delete mode 100644 Processor/Esolang.Processor.Abstractions/IProcessor.cs delete mode 100644 coverlet.collect.runsettings diff --git a/.editorconfig b/.editorconfig index 5348c94..d1cc9ea 100644 --- a/.editorconfig +++ b/.editorconfig @@ -39,7 +39,6 @@ dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning # Accessibility modifier settings -dotnet_style_require_accessibility_modifiers = omit_if_default # Expression-level preferences dotnet_style_coalesce_expression = true:warning diff --git a/.github/workflows/aot.yml b/.github/workflows/aot.yml new file mode 100644 index 0000000..1caeb0f --- /dev/null +++ b/.github/workflows/aot.yml @@ -0,0 +1,61 @@ +name: native-aot-ci + +on: + push: + branches: [ main ] + paths: + - 'Interpreter/**' + - 'Processor/**' + - '.github/workflows/aot.yml' + pull_request: + branches: [ main ] + paths: + - 'Interpreter/**' + - 'Processor/**' + +env: + DOTNET_VERSION: '10.0.x' + PROJECT_PATH: 'Interpreter/Esolang.Funge.Interpreter.csproj' + +jobs: + build: + name: build-aot-${{ matrix.rid }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: windows-latest + rid: win-x64 + - os: ubuntu-latest + rid: linux-x64 + - os: macos-latest + rid: osx-x64 + - os: macos-latest + rid: osx-arm64 + - os: ubuntu-latest + rid: any + + steps: + - uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' && matrix.rid != 'any' + run: | + sudo apt-get update + sudo apt-get install -y clang zlib1g-dev + + - name: Pack + run: | + dotnet pack ${{ env.PROJECT_PATH }} -r ${{ matrix.rid }} -c Release -o artifacts/ + + - name: Upload Artifact (CI) + uses: actions/upload-artifact@v7 + with: + name: nupkg-ci-${{ matrix.rid }} + path: artifacts/* + retention-days: 1 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5796056..5bfc960 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -3,82 +3,110 @@ name: build and test on: push: pull_request: - branches: [ master ] + branches: [ main ] paths: - - '**.cs' - - '**.csproj' - - '**.slnx' - - '.github/workflows/**' + - '**.cs' + - '**.csproj' + - '**.slnx' + - '**.props' + - '**.targets' + - 'global.json' + - '.github/workflows/**' env: DOTNET_VERSION: '10.0.x' - NUGET_SOURCE: 'https://api.nuget.org/v3/index.json' jobs: build-and-test: - - name: build-and-test-${{matrix.os}} + name: build-and-test-${{ matrix.rid }} runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + include: + - os: ubuntu-latest + rid: linux-x64 + - os: windows-latest + rid: win-x64 + - os: macOS-latest + rid: osx-arm64 steps: - - uses: actions/checkout@v4 - - name: Setup .NET Core - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Install dependencies - run: dotnet restore --source "${{ env.NUGET_SOURCE }}" - - - name: Build - run: dotnet build --no-restore - - - name: Test - run: dotnet test --no-restore --verbosity normal - - - name: Tool E2E (pack/install/run) - if: ${{ matrix.os == 'ubuntu-latest' }} - shell: bash - run: | - set -euo pipefail - rm -rf .artifacts/tool-e2e - mkdir -p .artifacts/tool-e2e - - dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -o .artifacts/tool-e2e - - nupkg_path=$(find .artifacts/tool-e2e -maxdepth 1 -type f -name 'dotnet-funge.*.nupkg' | head -n 1) - if [ -z "$nupkg_path" ]; then - echo "Failed to find dotnet-funge nupkg in .artifacts/tool-e2e" - exit 1 - fi - nupkg_name=$(basename "$nupkg_path") - tool_version=${nupkg_name#dotnet-funge.} - tool_version=${tool_version%.nupkg} - echo "Detected tool version: $tool_version" - - dotnet tool install dotnet-funge \ - --tool-path .artifacts/tool-e2e/path \ - --add-source .artifacts/tool-e2e \ - --version "$tool_version" - - ./.artifacts/tool-e2e/path/dotnet-funge --help - - output=$(./.artifacts/tool-e2e/path/dotnet-funge "samples/Generator.UseConsole/Programs/hello.b98") - echo "$output" - test "$output" = "Hello, World!" - - dotnet tool uninstall dotnet-funge --tool-path .artifacts/tool-e2e/path - - - name: Pack - if: ${{ matrix.os == 'ubuntu-latest' }} - run: | - dotnet pack -o artifacts/ - - - uses: actions/upload-artifact@v4 - if: ${{ matrix.os == 'ubuntu-latest' }} - with: - name: artifacts - path: artifacts + - uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y clang zlib1g-dev + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore + + - name: Test + run: dotnet test --no-build --verbosity normal + + - name: Tool E2E (pack/install/run) + shell: bash + run: | + set -euo pipefail + rid="${{ matrix.rid }}" + rm -rf .artifacts/tool-e2e + mkdir -p .artifacts/tool-e2e + + dotnet pack Interpreter/Esolang.Piet.Interpreter.csproj -r "$rid" -p:ToolPackageRuntimeIdentifiers= -o .artifacts/tool-e2e + + nupkg_path=$(find .artifacts/tool-e2e -maxdepth 1 -type f -name "dotnet-piet.*.nupkg" ! -name "*.snupkg" | head -n 1) + if [ -z "$nupkg_path" ]; then + echo "Failed to find dotnet-piet nupkg in .artifacts/tool-e2e" + exit 1 + fi + nupkg_name=$(basename "$nupkg_path") + tool_version=${nupkg_name#dotnet-piet.} + tool_version=${tool_version%.nupkg} + echo "Detected tool version: $tool_version" + + dotnet new tool-manifest --force + dotnet tool install dotnet-piet \ + --add-source .artifacts/tool-e2e \ + --version "$tool_version" + + dotnet tool run dotnet-piet -- --help + + # hello-world.png → "Hello, world!" を検証 + output1=$(dotnet tool run dotnet-piet -- samples/Generator.UseConsole/samples/hello-world.png) + echo "$output1" + test "$output1" = "Hello, world!" + + # parse サブコマンドで ascii-piet テキストが出力されることを確認 + output2=$(dotnet tool run dotnet-piet -- parse samples/Generator.UseConsole/samples/hello-world.png) + echo "$output2" + [ -n "$output2" ] + + # inline funge テキストで実行 + output3=$(dotnet tool run dotnet-funge -- --ascii-piet-text "l_ C") + echo "$output3" + + dotnet tool uninstall dotnet-funge + + - name: Pack + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + dotnet pack Generator/Esolang.Funge.Generator.csproj -o artifacts/ + dotnet pack Parser/Esolang.Funge.Parser.csproj -o artifacts/ + dotnet pack Processor/Esolang.Funge.Processor.csproj -o artifacts/ + dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -p:ToolPackageRuntimeIdentifiers= -o artifacts/ + dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -p:PublishAot=false -p:ToolPackageRuntimeIdentifiers="" -o artifacts/ + + - uses: actions/upload-artifact@v7 + if: ${{ matrix.os == 'ubuntu-latest' }} + with: + name: artifacts + path: artifacts/ diff --git a/Directory.Build.props b/Directory.Build.props index f7d43d3..0dce95b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,9 +3,9 @@ enable enable 14 - 1.1.1.4 - 1.1.1.4 - 1.1.1 + 2.0.0.0 + 2.0.0.5 + 2.0.0 https://github.com/Esolang-NET/Funge/ https://github.com/Esolang-NET/Funge.git true diff --git a/Directory.Build.targets b/Directory.Build.targets index 4d63d3d..b738cef 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -4,7 +4,9 @@ - + + + diff --git a/Generator/Esolang.Funge.Generator.csproj b/Generator/Esolang.Funge.Generator.csproj index 9d64802..62dd6b9 100644 --- a/Generator/Esolang.Funge.Generator.csproj +++ b/Generator/Esolang.Funge.Generator.csproj @@ -18,6 +18,7 @@ + all runtime; build; native; contentfiles; analyzers @@ -43,6 +44,8 @@ + + diff --git a/Generator/Esolang.Generator.Abstractions/.editorconfig b/Generator/Esolang.Generator.Abstractions/.editorconfig deleted file mode 100644 index 50c37f6..0000000 --- a/Generator/Esolang.Generator.Abstractions/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*] -generated_code = true diff --git a/Generator/Esolang.Generator.Abstractions/BindingError.cs b/Generator/Esolang.Generator.Abstractions/BindingError.cs deleted file mode 100644 index 3d8e89f..0000000 --- a/Generator/Esolang.Generator.Abstractions/BindingError.cs +++ /dev/null @@ -1,110 +0,0 @@ -#nullable enable -using Microsoft.CodeAnalysis; -using System.Diagnostics.CodeAnalysis; - -namespace Esolang.Generator; - -/// -/// Specifies the kind of diagnostic error that occurred during method signature binding. -/// -public enum BindingErrorKind -{ - /// The return type of the method is not supported. - UnsupportedReturnType, - /// A parameter has an invalid modifier (e.g., ref, out, in). - InvalidParameterModifier, - /// More than one parameter is competing for the same input role. - DuplicateInput, - /// More than one parameter is competing for the same output role. - DuplicateOutput, - /// More than one cancellation token parameter was found. - DuplicateCancellationToken, - /// More than one logger parameter was found. - DuplicateLogger, - /// A conflict exists between the return type and an output parameter. - ReturnOutputConflict, -} - -/// -/// Represents a diagnostic error that occurred during method signature binding. -/// -[ExcludeFromCodeCoverage] -public abstract record BindingError -{ - /// - /// - /// - /// The kind of error. - /// The location associated with the error. - BindingError(BindingErrorKind Kind, Location? Location) : base() - => (this.Kind, this.Location) = (Kind, Location); - - /// - /// The kind of error. - /// - public BindingErrorKind Kind { get; } - - /// - /// The location associated with the error. - /// - public Location? Location { get; } - - /// - /// The return type of the method is not supported. - /// - /// The unsupported return type symbol. - /// The location of the return type. - public sealed record UnsupportedReturnType(ITypeSymbol ReturnType, Location? Location) - : BindingError(BindingErrorKind.UnsupportedReturnType, Location); - - /// - /// A parameter has an invalid modifier (e.g., ref, out, in). - /// - /// The parameter with the invalid modifier. - /// The location of the parameter. - public sealed record InvalidParameterModifier(IParameterSymbol Parameter, Location? Location) - : BindingError(BindingErrorKind.InvalidParameterModifier, Location); - - /// - /// More than one parameter is competing for the same input role. - /// - /// The parameter that caused the duplication. - /// The kind of input that was already assigned. - /// The location of the duplicate parameter. - public sealed record DuplicateInput(IParameterSymbol Parameter, MethodInputKind ExistingKind, Location? Location) - : BindingError(BindingErrorKind.DuplicateInput, Location); - - /// - /// More than one parameter is competing for the same output role. - /// - /// The parameter that caused the duplication. - /// The kind of output that was already assigned. - /// The location of the duplicate parameter. - public sealed record DuplicateOutput(IParameterSymbol Parameter, MethodOutputKind ExistingKind, Location? Location) - : BindingError(BindingErrorKind.DuplicateOutput, Location); - - /// - /// More than one cancellation token parameter was found. - /// - /// The duplicate cancellation token parameter. - /// The location of the duplicate parameter. - public sealed record DuplicateCancellationToken(IParameterSymbol Parameter, Location? Location) - : BindingError(BindingErrorKind.DuplicateCancellationToken, Location); - - /// - /// More than one logger parameter was found. - /// - /// The duplicate logger parameter. - /// The location of the duplicate parameter. - public sealed record DuplicateLogger(IParameterSymbol Parameter, Location? Location) - : BindingError(BindingErrorKind.DuplicateLogger, Location); - - /// - /// A conflict exists between the return type and an output parameter. - /// - /// The output parameter that conflicts with the return type. - /// The location of the conflicting parameter. - public sealed record ReturnOutputConflict(IParameterSymbol Parameter, Location? Location) - : BindingError(BindingErrorKind.ReturnOutputConflict, Location); - -} diff --git a/Generator/Esolang.Generator.Abstractions/IsExternalInit.cs b/Generator/Esolang.Generator.Abstractions/IsExternalInit.cs deleted file mode 100644 index f76ff9f..0000000 --- a/Generator/Esolang.Generator.Abstractions/IsExternalInit.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace System.Runtime.CompilerServices; - -#if !NET5_0_OR_GREATER -/// -/// Reserved to be used by the compiler for tracking metadata. -/// This class should not be used by developers in source code. -/// -static class IsExternalInit -{ -} -#endif diff --git a/Generator/Esolang.Generator.Abstractions/KnownTypes.cs b/Generator/Esolang.Generator.Abstractions/KnownTypes.cs deleted file mode 100644 index f280325..0000000 --- a/Generator/Esolang.Generator.Abstractions/KnownTypes.cs +++ /dev/null @@ -1,250 +0,0 @@ -#nullable enable -using Microsoft.CodeAnalysis; -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace Esolang.Generator; - -/// -/// Holds resolved type symbols for a compilation. -/// -/// -/// Initializes a new instance of the struct. -/// -/// The compilation to resolve types from. -[ExcludeFromCodeCoverage] -public readonly struct KnownTypes(Compilation compilation) -{ - /// The string type symbol. - public readonly INamedTypeSymbol? String = compilation.GetSpecialType(SpecialType.System_String); - /// The byte type symbol. - public readonly INamedTypeSymbol? Byte = compilation.GetSpecialType(SpecialType.System_Byte); - /// The int type symbol. - public readonly INamedTypeSymbol? Int32 = compilation.GetSpecialType(SpecialType.System_Int32); - /// The System.Threading.Tasks.Task type symbol. - public readonly INamedTypeSymbol? Task = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task"); - /// The System.Threading.Tasks.Task{TResult} type symbol. - public readonly INamedTypeSymbol? TaskT = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1"); - /// The System.Threading.Tasks.ValueTask type symbol. - public readonly INamedTypeSymbol? ValueTask = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask"); - /// The System.Threading.Tasks.ValueTask{TResult} type symbol. - public readonly INamedTypeSymbol? ValueTaskT = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); - /// The System.Collections.Generic.IEnumerable{T} type symbol. - public readonly INamedTypeSymbol? IEnumerableT = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IEnumerable`1"); - /// The System.Collections.Generic.IAsyncEnumerable{T} type symbol. - public readonly INamedTypeSymbol? IAsyncEnumerableT = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1"); - /// The System.IO.Pipelines.PipeReader type symbol. - public readonly INamedTypeSymbol? PipeReader = compilation.GetBestTypeByMetadataName("System.IO.Pipelines.PipeReader"); - /// The System.IO.Pipelines.PipeWriter type symbol. - public readonly INamedTypeSymbol? PipeWriter = compilation.GetBestTypeByMetadataName("System.IO.Pipelines.PipeWriter"); - /// The System.IO.TextReader type symbol. - public readonly INamedTypeSymbol? TextReader = compilation.GetBestTypeByMetadataName("System.IO.TextReader"); - /// The System.IO.TextWriter type symbol. - public readonly INamedTypeSymbol? TextWriter = compilation.GetBestTypeByMetadataName("System.IO.TextWriter"); - /// The System.Threading.CancellationToken type symbol. - public readonly INamedTypeSymbol? CancellationToken = compilation.GetBestTypeByMetadataName("System.Threading.CancellationToken"); - /// The Microsoft.Extensions.Logging.ILogger type symbol. - public readonly INamedTypeSymbol? ILogger = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger"); - /// The Microsoft.Extensions.Logging.ILogger{T} type symbol. - public readonly INamedTypeSymbol? ILoggerT = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger`1"); - - static bool EqualsDefinition(ITypeSymbol? type, ISymbol? symbol) => - type != null && symbol != null && SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, symbol); - - static bool EqualsType(ITypeSymbol? type, ISymbol? symbol) => - type != null && symbol != null && SymbolEqualityComparer.Default.Equals(type, symbol); - - /// Gets a value indicating whether the type is string. - /// The type to check. - /// Optional: Whether to check for nullability. - public readonly bool IsString(ITypeSymbol? type, bool? isNullable = null) - { - if (type is not INamedTypeSymbol named || !SymbolEqualityComparer.Default.Equals(named, String)) return false; - if (isNullable == null) return true; - if (isNullable.Value) return type.NullableAnnotation == NullableAnnotation.Annotated; - return type.NullableAnnotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; - } - - /// Gets a value indicating whether the type is byte. - public readonly bool IsByte(ITypeSymbol? type) => EqualsType(type, Byte); - /// Gets a value indicating whether the type is int. - public readonly bool IsInt32(ITypeSymbol? type) => EqualsType(type, Int32); - - /// Gets a value indicating whether the type is System.Threading.Tasks.Task. - public readonly bool IsTask(ITypeSymbol? type) => EqualsType(type, Task); - /// Gets a value indicating whether the type is System.Threading.Tasks.Task{TResult}. - public readonly bool IsTaskT(ITypeSymbol? type, bool? isNullable = null) - { - if (type is not INamedTypeSymbol named || !EqualsDefinition(named, TaskT)) return false; - if (isNullable == null) return true; - var annotation = named.TypeArguments[0].NullableAnnotation; - return isNullable.Value ? annotation == NullableAnnotation.Annotated : annotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; - } - /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask. - public readonly bool IsValueTask(ITypeSymbol? type) => EqualsType(type, ValueTask); - /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{TResult}. - public readonly bool IsValueTaskT(ITypeSymbol? type, bool? isNullable = null) - { - if (type is not INamedTypeSymbol named || !EqualsDefinition(named, ValueTaskT)) return false; - if (isNullable == null) return true; - var annotation = named.TypeArguments[0].NullableAnnotation; - return isNullable.Value ? annotation == NullableAnnotation.Annotated : annotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; - } - /// Gets a value indicating whether the type is System.Collections.Generic.IEnumerable{T}. - public readonly bool IsIEnumerableT(ITypeSymbol? type) => EqualsDefinition(type, IEnumerableT); - /// Gets a value indicating whether the type is System.Collections.Generic.IAsyncEnumerable{T}. - public readonly bool IsIAsyncEnumerableT(ITypeSymbol? type) => EqualsDefinition(type, IAsyncEnumerableT); - - /// Gets a value indicating whether the type is System.IO.Pipelines.PipeReader. - public readonly bool IsPipeReader(ITypeSymbol? type) => EqualsType(type, PipeReader); - /// Gets a value indicating whether the type is System.IO.Pipelines.PipeWriter. - public readonly bool IsPipeWriter(ITypeSymbol? type) => EqualsType(type, PipeWriter); - /// Gets a value indicating whether the type is System.IO.TextReader. - public readonly bool IsTextReader(ITypeSymbol? type) => EqualsType(type, TextReader); - /// Gets a value indicating whether the type is System.IO.TextWriter. - public readonly bool IsTextWriter(ITypeSymbol? type) => EqualsType(type, TextWriter); - /// Gets a value indicating whether the type is System.Threading.CancellationToken. - public readonly bool IsCancellationToken(ITypeSymbol? type) => EqualsType(type, CancellationToken); - - /// Gets a value indicating whether the type is System.Threading.Tasks.Task{String}. - public readonly bool IsTaskString(ITypeSymbol? type, bool? isNullable = null) => IsTaskT(type, isNullable) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_String; - /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{String}. - public readonly bool IsValueTaskString(ITypeSymbol? type, bool? isNullable = null) => IsValueTaskT(type, isNullable) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_String; - - /// Gets a value indicating whether the type is System.Threading.Tasks.Task{Int32}. - public readonly bool IsTaskInt32(ITypeSymbol? type) => IsTaskT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Int32; - /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{Int32}. - public readonly bool IsValueTaskInt32(ITypeSymbol? type) => IsValueTaskT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Int32; - - /// Gets a value indicating whether the type is System.Collections.Generic.IEnumerable{Byte}. - public readonly bool IsIEnumerableByte(ITypeSymbol? type) => IsIEnumerableT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Byte; - /// Gets a value indicating whether the type is System.Collections.Generic.IAsyncEnumerable{Byte}. - public readonly bool IsIAsyncEnumerableByte(ITypeSymbol? type) => IsIAsyncEnumerableT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Byte; - - /// Gets a value indicating whether the type is a logger type (ILogger or ILogger{T}). - public readonly bool IsLogger(ITypeSymbol? type) - { - if (type == null) return false; - if (EqualsType(type, ILogger) || EqualsDefinition(type, ILoggerT)) return true; - foreach (var iface in type.AllInterfaces) - { - if (EqualsType(iface, ILogger) || EqualsDefinition(iface, ILoggerT)) return true; - } - return false; - } - - [ExcludeFromCodeCoverage] - readonly bool PrintMembers(StringBuilder builder) - { - builder.Append(nameof(String)).Append('='); - AppendNamedTypeSymbol(String, builder); - builder.Append(", "); - - builder.Append(nameof(Byte)).Append('='); - AppendNamedTypeSymbol(Byte, builder); - builder.Append(", "); - - builder.Append(nameof(Int32)).Append('='); - AppendNamedTypeSymbol(Int32, builder); - builder.Append(", "); - - builder.Append(nameof(Task)).Append('='); - AppendNamedTypeSymbol(Task, builder); - builder.Append(", "); - - builder.Append(nameof(TaskT)).Append('='); - AppendNamedTypeSymbol(TaskT, builder); - builder.Append(", "); - - builder.Append(nameof(ValueTask)).Append('='); - AppendNamedTypeSymbol(ValueTask, builder); - builder.Append(", "); - - builder.Append(nameof(ValueTaskT)).Append('='); - AppendNamedTypeSymbol(ValueTaskT, builder); - builder.Append(", "); - - builder.Append(nameof(IEnumerableT)).Append('='); - AppendNamedTypeSymbol(IEnumerableT, builder); - builder.Append(", "); - - builder.Append(nameof(IAsyncEnumerableT)).Append('='); - AppendNamedTypeSymbol(IAsyncEnumerableT, builder); - builder.Append(", "); - - builder.Append(nameof(PipeReader)).Append('='); - AppendNamedTypeSymbol(PipeReader, builder); - builder.Append(", "); - - builder.Append(nameof(PipeWriter)).Append('='); - AppendNamedTypeSymbol(PipeWriter, builder); - builder.Append(", "); - - builder.Append(nameof(TextReader)).Append('='); - AppendNamedTypeSymbol(TextReader, builder); - builder.Append(", "); - - builder.Append(nameof(TextWriter)).Append('='); - AppendNamedTypeSymbol(TextWriter, builder); - builder.Append(", "); - - builder.Append(nameof(CancellationToken)).Append('='); - AppendNamedTypeSymbol(CancellationToken, builder); - builder.Append(", "); - - builder.Append(nameof(ILogger)).Append('='); - AppendNamedTypeSymbol(ILogger, builder); - builder.Append(", "); - - builder.Append(nameof(ILoggerT)).Append('='); - AppendNamedTypeSymbol(ILoggerT, builder); - - return true; - static void AppendNamedTypeSymbol(INamedTypeSymbol? symbol, StringBuilder builder) - { - if (symbol == null) return; - builder.Append('('); - builder.Append(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - builder.Append(", "); - builder.Append(nameof(symbol.NullableAnnotation)).Append('=').Append(symbol.NullableAnnotation); - builder.Append(')'); - } - } - - /// - [ExcludeFromCodeCoverage] - public override string ToString() - { - var builder = new StringBuilder(); - builder.Append(nameof(KnownTypes)).Append(" {"); - if (!PrintMembers(builder)) - { - builder.Append(' '); - } - builder.Append('}'); - return builder.ToString(); - } -} - -/// -/// Provides utility methods for resolving types from a . -/// -[ExcludeFromCodeCoverage] -public static class TypeResolutionExtensions -{ - /// - /// Resolves the best for the specified metadata name. - /// - public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, string metadataName) - { - var type = compilation.GetTypeByMetadataName(metadataName); - if (type != null) return type; - - foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols) - { - var found = assembly.GetTypeByMetadataName(metadataName); - if (found != null) return found; - } - return null; - } -} diff --git a/Generator/Esolang.Generator.Abstractions/MethodInputKind.cs b/Generator/Esolang.Generator.Abstractions/MethodInputKind.cs deleted file mode 100644 index 59f5282..0000000 --- a/Generator/Esolang.Generator.Abstractions/MethodInputKind.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable enable -namespace Esolang.Generator; - -/// -/// Specifies the input mechanism of the generated method. -/// -public enum MethodInputKind -{ - /// No explicit input mechanism. - None, - /// Input is provided via a string parameter. - String, - /// Input is provided via a TextReader parameter. - TextReader, - /// Input is provided via a PipeReader parameter. - PipeReader, -} diff --git a/Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs b/Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs deleted file mode 100644 index 4d48f81..0000000 --- a/Generator/Esolang.Generator.Abstractions/MethodOutputKind.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Esolang.Generator; - -/// -/// Specifies the output mechanism of the generated method. -/// -public enum MethodOutputKind -{ - /// No explicit output mechanism. - None, - /// Output is written to a TextWriter parameter. - TextWriter, - /// Output is written to a PipeWriter parameter. - PipeWriter, - /// Output is returned as a string. - ReturnString, - /// Output is yielded via IEnumerable<byte>. - ReturnIEnumerable, - /// Output is yielded via IAsyncEnumerable<byte>. - ReturnIAsyncEnumerable, -} diff --git a/Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs b/Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs deleted file mode 100644 index f2ae6f6..0000000 --- a/Generator/Esolang.Generator.Abstractions/MethodReturnKind.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Esolang.Generator; - -/// -/// Specifies the return type of the generated method. -/// -public enum MethodReturnKind -{ - /// The return type is invalid or unsupported. - Invalid, - /// The method returns void. - Void, - /// The method returns int. - Int32, - /// The method returns string. - String, - /// The method returns string (nullable). - NullableString, - /// The method returns Task. - Task, - /// The method returns Task<int>. - TaskInt32, - /// The method returns Task<string>. - TaskString, - /// The method returns Task<string?>. - TaskNullableString, - /// The method returns ValueTask. - ValueTask, - /// The method returns ValueTask<int>. - ValueTaskInt32, - /// The method returns ValueTask<string>. - ValueTaskString, - /// The method returns ValueTask<string?>. - ValueTaskNullableString, - /// The method returns IEnumerable<byte>. - IEnumerableByte, - /// The method returns IAsyncEnumerable<byte>. - IAsyncEnumerableByte, -} diff --git a/Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs b/Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs deleted file mode 100644 index 3f88aec..0000000 --- a/Generator/Esolang.Generator.Abstractions/MethodSignatureBinder.cs +++ /dev/null @@ -1,228 +0,0 @@ -#nullable enable -using Microsoft.CodeAnalysis; -using System.Diagnostics.CodeAnalysis; -using static Esolang.Generator.BindingError; - -namespace Esolang.Generator; - -/// -/// Provides utility methods for binding method signatures to . -/// -[ExcludeFromCodeCoverage] -public static class MethodSignatureBinder -{ - /// - /// Binds the specified method symbol to a . - /// - /// The method symbol to bind. - /// The known types for the compilation. - /// The result of the binding. - public static MethodSignatureBinding Bind( - IMethodSymbol method, - KnownTypes types) - { - var returnKind = BindReturnKind(method.ReturnType, types); - if (returnKind == MethodReturnKind.Invalid) - { - return new MethodSignatureBinding(returnKind, MethodInputKind.None, MethodOutputKind.None, "", "", null, null, false, method.Parameters, new UnsupportedReturnType(method.ReturnType, method.Locations.FirstOrDefault())); - } - - var outputKind = BindDefaultOutputKind(returnKind); - var inputKind = MethodInputKind.None; - var inputExpr = ""; - var outputExpr = ""; - string? cancellationTokenName = null; - string? loggerExpression = null; - var isLoggerFromParameter = false; - var unhandledParameters = new List(); - - foreach (var p in method.Parameters) - { - if (p.RefKind != RefKind.None) - { - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new InvalidParameterModifier(p, p.Locations.FirstOrDefault())); - } - - if (types.IsString(p.Type, false)) - { - if (inputKind != MethodInputKind.None) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); - - inputKind = MethodInputKind.String; - inputExpr = p.Name; - continue; - } - - if (types.IsTextReader(p.Type)) - { - if (inputKind != MethodInputKind.None) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); - - inputKind = MethodInputKind.TextReader; - inputExpr = p.Name; - continue; - } - - if (types.IsPipeReader(p.Type)) - { - if (inputKind != MethodInputKind.None) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); - - inputKind = MethodInputKind.PipeReader; - inputExpr = p.Name; - continue; - } - - if (types.IsTextWriter(p.Type)) - { - if (IsOutputReturning(returnKind)) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new ReturnOutputConflict(p, p.Locations.FirstOrDefault())); - - if (outputKind != MethodOutputKind.None) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateOutput(p, outputKind, p.Locations.FirstOrDefault())); - - outputKind = MethodOutputKind.TextWriter; - outputExpr = p.Name; - continue; - } - - if (types.IsPipeWriter(p.Type)) - { - if (IsOutputReturning(returnKind)) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new ReturnOutputConflict(p, p.Locations.FirstOrDefault())); - - if (outputKind != MethodOutputKind.None) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateOutput(p, outputKind, p.Locations.FirstOrDefault())); - - outputKind = MethodOutputKind.PipeWriter; - outputExpr = p.Name; - continue; - } - - if (types.IsCancellationToken(p.Type)) - { - if (cancellationTokenName != null) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateCancellationToken(p, p.Locations.FirstOrDefault())); - - cancellationTokenName = p.Name; - continue; - } - - if (types.IsLogger(p.Type)) - { - if (loggerExpression != null) - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateLogger(p, p.Locations.FirstOrDefault())); - - loggerExpression = p.Name; - isLoggerFromParameter = true; - continue; - } - - unhandledParameters.Add(p); - } - - loggerExpression ??= FindLoggerInContainingType(method.ContainingType, method.IsStatic, types, out isLoggerFromParameter); - - return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, unhandledParameters); - } - - /// - /// Binds the return type symbol to a . - /// - public static MethodReturnKind BindReturnKind(ITypeSymbol returnType, KnownTypes types) - { - if (returnType.SpecialType == SpecialType.System_Void) return MethodReturnKind.Void; - if (returnType.SpecialType == SpecialType.System_Int32) return MethodReturnKind.Int32; - if (types.IsString(returnType, false)) return MethodReturnKind.String; - if (types.IsString(returnType, true)) return MethodReturnKind.NullableString; - if (types.IsTask(returnType)) return MethodReturnKind.Task; - if (types.IsTaskInt32(returnType)) return MethodReturnKind.TaskInt32; - if (types.IsTaskString(returnType, false)) return MethodReturnKind.TaskString; - if (types.IsTaskString(returnType, true)) return MethodReturnKind.TaskNullableString; - if (types.IsValueTask(returnType)) return MethodReturnKind.ValueTask; - if (types.IsValueTaskInt32(returnType)) return MethodReturnKind.ValueTaskInt32; - if (types.IsValueTaskString(returnType, false)) return MethodReturnKind.ValueTaskString; - if (types.IsValueTaskString(returnType, true)) return MethodReturnKind.ValueTaskNullableString; - if (types.IsIEnumerableByte(returnType)) return MethodReturnKind.IEnumerableByte; - if (types.IsIAsyncEnumerableByte(returnType)) return MethodReturnKind.IAsyncEnumerableByte; - - return MethodReturnKind.Invalid; - } - - /// - /// Gets the default output kind based on the return kind. - /// - static MethodOutputKind BindDefaultOutputKind(MethodReturnKind returnKind) => returnKind switch - { - MethodReturnKind.String or MethodReturnKind.NullableString or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString => MethodOutputKind.ReturnString, - MethodReturnKind.IEnumerableByte => MethodOutputKind.ReturnIEnumerable, - MethodReturnKind.IAsyncEnumerableByte => MethodOutputKind.ReturnIAsyncEnumerable, - _ => MethodOutputKind.None - }; - - /// - /// Gets a value indicating whether the return kind implies output is returned. - /// - static bool IsOutputReturning(MethodReturnKind returnKind) => returnKind switch - { - MethodReturnKind.String or MethodReturnKind.NullableString or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString or MethodReturnKind.IEnumerableByte or MethodReturnKind.IAsyncEnumerableByte => true, - _ => false - }; - - /// - /// Searches for a logger in the containing type (fields or constructor parameters). - /// - /// The type to search in. - /// Whether the target method is static. - /// The known types for the compilation. - /// Output: Whether the logger was found in a constructor parameter. - /// The expression to access the logger, or null if not found. - static string? FindLoggerInContainingType(ITypeSymbol? type, bool isStatic, KnownTypes types, out bool isFromParameter) - { - isFromParameter = false; - var currentType = type; - var shadowedNames = new HashSet(StringComparer.Ordinal); - var isBaseType = false; - - while (currentType != null) - { - foreach (var field in currentType.GetMembers().OfType()) - { - if (isStatic && !field.IsStatic) continue; - - // If searching in a base type, the field must be accessible (protected or public) - if (isBaseType && field.DeclaredAccessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal or Accessibility.Public or Accessibility.Internal)) - continue; - - if (types.IsLogger(field.Type)) - { - return field.Name; - } - - if (field.CanBeReferencedByName) - { - shadowedNames.Add(field.Name); - } - } - currentType = currentType.BaseType; - isBaseType = true; - } - - if (type is INamedTypeSymbol namedType) - { - foreach (var constructor in namedType.InstanceConstructors) - { - foreach (var parameter in constructor.Parameters) - { - if (types.IsLogger(parameter.Type) && !shadowedNames.Contains(parameter.Name)) - { - isFromParameter = true; - return parameter.Name; - } - } - } - } - - return null; - } -} diff --git a/Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs b/Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs deleted file mode 100644 index b5b93c0..0000000 --- a/Generator/Esolang.Generator.Abstractions/MethodSignatureBinding.cs +++ /dev/null @@ -1,98 +0,0 @@ -#nullable enable -using Microsoft.CodeAnalysis; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace Esolang.Generator; - -/// -/// Represents the result of binding a method signature for generation. -/// -/// The return kind of the method. -/// The input kind of the method. -/// The output kind of the method. -/// The expression to access the input (e.g., parameter name). -/// The expression to access the output (e.g., parameter name). -/// The name of the cancellation token parameter, if any. -/// The expression to access the logger (e.g., "loggerParam", "this._logger"). -/// Whether the logger is obtained from a method parameter. -/// Parameters that were not handled by the common binding logic. -/// The diagnostic error if the binding failed. -[DebuggerDisplay("{ToString(),nq}")] -[ExcludeFromCodeCoverage] -public record struct MethodSignatureBinding( - MethodReturnKind ReturnKind, - MethodInputKind InputKind, - MethodOutputKind OutputKind, - string InputExpression, - string OutputExpression, - string? CancellationTokenName, - string? LoggerExpression, - bool IsLoggerFromParameter, - IReadOnlyList UnhandledParameters, - BindingError? Error = null) -{ - /// Whether the binding is successful. - [MemberNotNullWhen(false, nameof(Error))] - public readonly bool IsValid => Error is null; - - /// Gets a value indicating whether the method has an explicit input mechanism. - public readonly bool HasExplicitInput => InputKind != MethodInputKind.None; - - /// Gets a value indicating whether the method has an explicit output mechanism. - public readonly bool HasExplicitOutput => OutputKind != MethodOutputKind.None; - - /// Gets a value indicating whether the method is asynchronous. - public readonly bool IsAsync => ReturnKind switch - { - MethodReturnKind.Task or MethodReturnKind.TaskInt32 or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or - MethodReturnKind.ValueTask or MethodReturnKind.ValueTaskInt32 or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString or - MethodReturnKind.IAsyncEnumerableByte => true, - _ => false - }; - - /// Gets a value indicating whether the method returns an enumerable. - public readonly bool IsEnumerable => ReturnKind == MethodReturnKind.IEnumerableByte; - - /// Gets a value indicating whether the method returns an async enumerable. - public readonly bool IsAsyncEnumerable => ReturnKind == MethodReturnKind.IAsyncEnumerableByte; - - [ExcludeFromCodeCoverage] - readonly bool PrintMembers(StringBuilder builder) - { - builder.Append(nameof(IsValid)).Append('=').Append(IsValid).Append(", "); - builder.Append(nameof(ReturnKind)).Append('=').Append(ReturnKind).Append(", "); - builder.Append(nameof(InputKind)).Append('=').Append(InputKind).Append(", "); - builder.Append(nameof(OutputKind)).Append('=').Append(OutputKind).Append(", "); - builder.Append(nameof(InputExpression)).Append('=').Append(InputExpression).Append(", "); - builder.Append(nameof(OutputExpression)).Append('=').Append(OutputExpression).Append(", "); - builder.Append(nameof(CancellationTokenName)).Append('=').Append(CancellationTokenName).Append(", "); - builder.Append(nameof(LoggerExpression)).Append('=').Append(LoggerExpression).Append(", "); - builder.Append(nameof(IsLoggerFromParameter)).Append('=').Append(IsLoggerFromParameter).Append(", "); - builder.Append(nameof(UnhandledParameters)).Append("=["); - for (var i = 0; i < UnhandledParameters.Count; i++) - { - if (i > 0) builder.Append(", "); - builder.Append(UnhandledParameters[i].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - } - builder.Append("], "); - builder.Append(nameof(Error)).Append('=').Append(Error); - return true; - } - - /// - [ExcludeFromCodeCoverage] - public override readonly string ToString() - { - var builder = new StringBuilder(); - builder.Append(nameof(MethodSignatureBinding)).Append(" {"); - if (!PrintMembers(builder)) - { - builder.Append(' '); - } - builder.Append('}'); - return builder.ToString(); - } - -} diff --git a/Generator/buildTransitive/Esolang.Funge.Generator.targets b/Generator/buildTransitive/Esolang.Funge.Generator.targets index 138fd6c..4ec9022 100644 --- a/Generator/buildTransitive/Esolang.Funge.Generator.targets +++ b/Generator/buildTransitive/Esolang.Funge.Generator.targets @@ -24,4 +24,10 @@ FungeLogicalPath="%(_FungeGeneratedFile.LogicalPath)" /> + + + + + + diff --git a/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj b/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj index 2de7d45..df4e8b4 100644 --- a/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj +++ b/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj @@ -1,7 +1,7 @@ - net8.0;net9.0;net10.0 + net10.0 Funge.Interpreter.Tests diff --git a/Interpreter/Esolang.Funge.Interpreter.csproj b/Interpreter/Esolang.Funge.Interpreter.csproj index 8db253a..38c4057 100644 --- a/Interpreter/Esolang.Funge.Interpreter.csproj +++ b/Interpreter/Esolang.Funge.Interpreter.csproj @@ -1,7 +1,7 @@ Exe - net8.0;net9.0;net10.0 + net10.0 dotnet-funge Funge-98 console interpreter. true @@ -13,13 +13,11 @@ Command-line interpreter for Funge-98 (Befunge-98) programs. dotnet-funge esolang;funge;funge-98;befunge;interpreter;cli;dotnet-tool - - - - true - true - true - true + true + false + false + true + win-x64;linux-x64;osx-arm64;any @@ -29,6 +27,7 @@ + diff --git a/Interpreter/Esolang.Interpreter.Abstractions/.editorconfig b/Interpreter/Esolang.Interpreter.Abstractions/.editorconfig deleted file mode 100644 index 50c37f6..0000000 --- a/Interpreter/Esolang.Interpreter.Abstractions/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*] -generated_code = true diff --git a/Interpreter/Esolang.Interpreter.Abstractions/InterpreterExtensions.cs b/Interpreter/Esolang.Interpreter.Abstractions/InterpreterExtensions.cs deleted file mode 100644 index 9501c3a..0000000 --- a/Interpreter/Esolang.Interpreter.Abstractions/InterpreterExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -#nullable enable -using Esolang.Processor; -using System.Diagnostics.CodeAnalysis; -using static Esolang.Processor.IOEvent; - -namespace Esolang.Interpreter; - -/// -/// Provides extension methods for running in an interpreter context. -/// -[ExcludeFromCodeCoverage] -public static class InterpreterExtensions -{ - /// - /// Executes the processor using standard I/O (Console.In, Console.Out). - /// - /// The event processor. - /// The cancellation token. - /// The exit code. - public static async ValueTask RunToConsoleAsync( - this IEventProcessor processor, - CancellationToken cancellationToken = default) - { - await foreach (var ioEvent in processor.RunAsyncEnumerable(cancellationToken)) - { - switch (ioEvent) - { - case InputCharEvent charInput: - charInput.Write(await ReadCharFromConsoleAsync(cancellationToken)); - break; - case InputIntEvent intInput: - var line = await ReadLineFromConsoleAsync(cancellationToken); - if (int.TryParse(line, out var i)) - { - intInput.Write(i); - } - break; - case OutputCharEvent charOutput: - Console.Write(charOutput.Output); - break; - case OutputIntEvent intOutput: - Console.Write(intOutput.Output); - break; - case EndEvent end: - return end.ExitCode; - } - } - return 0; - } - - static async ValueTask ReadCharFromConsoleAsync(CancellationToken ct) - { - ct.ThrowIfCancellationRequested(); - var c = Console.In.Read(); - return c == -1 ? '\0' : (char)c; - } - - static async ValueTask ReadLineFromConsoleAsync(CancellationToken ct) => await Console.In.ReadLineAsync(ct); -} diff --git a/Parser/Esolang.Funge.Parser.csproj b/Parser/Esolang.Funge.Parser.csproj index 3ee9197..5e7b824 100644 --- a/Parser/Esolang.Funge.Parser.csproj +++ b/Parser/Esolang.Funge.Parser.csproj @@ -9,13 +9,6 @@ esolang;funge;funge-98;befunge;parser - - true - true - true - true - - true false diff --git a/Processor/Esolang.Funge.Processor.csproj b/Processor/Esolang.Funge.Processor.csproj index 1f45a68..57a389d 100644 --- a/Processor/Esolang.Funge.Processor.csproj +++ b/Processor/Esolang.Funge.Processor.csproj @@ -9,13 +9,6 @@ esolang;funge;funge-98;befunge;processor - - true - true - true - true - - true false @@ -29,6 +22,10 @@ + + + + diff --git a/Processor/Esolang.Processor.Abstractions/.editorconfig b/Processor/Esolang.Processor.Abstractions/.editorconfig deleted file mode 100644 index 50c37f6..0000000 --- a/Processor/Esolang.Processor.Abstractions/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*] -generated_code = true diff --git a/Processor/Esolang.Processor.Abstractions/IOEvent.cs b/Processor/Esolang.Processor.Abstractions/IOEvent.cs deleted file mode 100644 index d9124cc..0000000 --- a/Processor/Esolang.Processor.Abstractions/IOEvent.cs +++ /dev/null @@ -1,110 +0,0 @@ -#nullable enable -using System.Diagnostics.CodeAnalysis; - -namespace Esolang.Processor; - -/// -/// Represents an I/O event. -/// -[ExcludeFromCodeCoverage] -public abstract class IOEvent -{ - IOEvent() { } - - /// - /// Creates an event requesting a character input. - /// - /// The action to write the input character to the processor. - /// An event requesting a character input. - public static InputCharEvent InputChar(Action write) => new(write); - - /// - /// Creates an event requesting an integer input. - /// - /// The action to write the input integer to the processor. - /// An event requesting an integer input. - public static InputIntEvent InputInt(Action write) => new(write); - - /// - /// Creates an event that outputs a character. - /// - /// The character to output. - /// An event that outputs a character. - public static OutputCharEvent OutputChar(char output) => new(output); - - /// - /// Creates an event that outputs an integer. - /// - /// The integer to output. - /// An event that outputs an integer. - public static OutputIntEvent OutputInt(int output) => new(output); - - /// - /// Creates an event indicating the end of execution. - /// - /// The exit code. - /// An event indicating the end of execution. - public static EndEvent End(int exitCode) => new(exitCode); - - /// - /// Represents an event requesting a character input. - /// - public sealed class InputCharEvent(Action write) : IOEvent - { - /// - /// Writes the input character to the processor. - /// - /// The input character. - public void Write(char c) => write(c); - } - - /// - /// Represents an event requesting an integer input. - /// - /// The action to write the input integer to the processor. - public sealed class InputIntEvent(Action write) : IOEvent - { - /// - /// Writes the input integer to the processor. - /// - /// The input integer. - public void Write(int i) => write(i); - } - - /// - /// Represents an event that outputs a character. - /// - /// The character to output. - public sealed class OutputCharEvent(char Output) : IOEvent - { - /// - /// The character to output. - /// - public char Output { get; } = Output; - } - - /// - /// Represents an event that outputs an integer. - /// - /// The integer to output. - public sealed class OutputIntEvent(int Output) : IOEvent - { - /// - /// The integer to output. - /// - public int Output { get; } = Output; - } - - /// - /// Represents an event indicating the end of execution. - /// - /// The exit code. - public sealed class EndEvent(int exitCode) : IOEvent - { - /// - /// The exit code. - /// - public int ExitCode { get; } = exitCode; - } - -} diff --git a/Processor/Esolang.Processor.Abstractions/IProcessor.cs b/Processor/Esolang.Processor.Abstractions/IProcessor.cs deleted file mode 100644 index f93bdb2..0000000 --- a/Processor/Esolang.Processor.Abstractions/IProcessor.cs +++ /dev/null @@ -1,32 +0,0 @@ -#nullable enable - -namespace Esolang.Processor; - -/// -/// Common base interface for all processors. -/// -public interface IProcessor { } - -/// -/// Common base interface for processors that hold a program to be executed. -/// -/// The type of the parsed program. -public interface IProcessor : IProcessor -{ - /// The parsed program. - TProgram Program { get; } -} - -/// -/// Execution model based on a stream of I/O events. -/// -public interface IEventProcessor : IProcessor -{ - /// - /// Executes the processor and returns a stream of I/O events. - /// - /// The cancellation token. - /// An asynchronous stream of I/O events. - IAsyncEnumerable RunAsyncEnumerable( - CancellationToken cancellationToken = default); -} diff --git a/coverlet.collect.runsettings b/coverlet.collect.runsettings deleted file mode 100644 index 3c39003..0000000 --- a/coverlet.collect.runsettings +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - cobertura - - - - - diff --git a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.csproj b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.csproj index 7af6468..8abd83a 100644 --- a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.csproj +++ b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.csproj @@ -15,6 +15,15 @@ + + + + + + + From 0c84f1decd2e44b418234efa160d26d3c69a6320 Mon Sep 17 00:00:00 2001 From: juner Date: Wed, 3 Jun 2026 21:59:51 +0900 Subject: [PATCH 20/25] feat(interpreter): add --source/--path CLI inputs --- Interpreter.Tests/ProgramTests.cs | 29 +++++++++++++++++++++-- Interpreter/FungeInterpreterExtensions.cs | 29 +++++++++++++++++++---- Interpreter/README.md | 21 ++++++++++++---- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/Interpreter.Tests/ProgramTests.cs b/Interpreter.Tests/ProgramTests.cs index dce4fba..1e56786 100644 --- a/Interpreter.Tests/ProgramTests.cs +++ b/Interpreter.Tests/ProgramTests.cs @@ -38,7 +38,7 @@ public async Task Run_HelloWorld_ReturnsZero() try { await File.WriteAllTextAsync(path, "64+\"!dlroW ,olleH\">:#,_@", CancellationToken); - var exitCode = Run([path]); + var exitCode = Run(["--path", path]); Assert.AreEqual(0, exitCode); } finally @@ -48,6 +48,31 @@ public async Task Run_HelloWorld_ReturnsZero() } } + [TestMethod] + public void Run_SourceOptionWithMultilineCode_ReturnsZero() + { + const string source = "v\n>25*\"!dlroW ,olleH\",,,,@"; + var exitCode = Run(["--source", source]); + Assert.AreEqual(0, exitCode); + } + + [TestMethod] + public async Task Run_PathAndSourceTogether_ReturnsOne() + { + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.b98"); + try + { + await File.WriteAllTextAsync(path, "@", CancellationToken); + var exitCode = Run(["--path", path, "--source", "@"]); + Assert.AreEqual(1, exitCode); + } + finally + { + if (File.Exists(path)) + File.Delete(path); + } + } + [TestMethod] public async Task RunAsync_CancelledToken_StopsInfiniteProgram() { @@ -59,7 +84,7 @@ public async Task RunAsync_CancelledToken_StopsInfiniteProgram() using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); - var exitCode = await Program.RunAsync([path], cancellationToken: cancellation.Token); + var exitCode = await Program.RunAsync(["--path", path], cancellationToken: cancellation.Token); Assert.AreEqual(0, exitCode); } finally diff --git a/Interpreter/FungeInterpreterExtensions.cs b/Interpreter/FungeInterpreterExtensions.cs index 892b3e6..38225c5 100644 --- a/Interpreter/FungeInterpreterExtensions.cs +++ b/Interpreter/FungeInterpreterExtensions.cs @@ -16,26 +16,45 @@ public static class FungeInterpreterExtensions /// public static RootCommand BuildRootCommand() { - var pathArgument = new Argument("path") + var pathOption = new Option(name: "--path", aliases: ["-p"]) { Description = "Path to a Funge-98 source file (.b98).", }; + var sourceOption = new Option(name: "--source", aliases: ["-s"]) + { + Description = "Inline Funge-98 source code. Newlines are supported.", + }; + var rootCommand = new RootCommand("Run Funge-98 (Befunge-98) programs.") { - pathArgument, + pathOption, + sourceOption, }; rootCommand.SetAction(async (parseResult, cancellationToken) => { - var path = parseResult.GetValue(pathArgument)!; - var space = FungeParser.ParseFile(path); + var path = parseResult.GetValue(pathOption); + var source = parseResult.GetValue(sourceOption); + + var hasPath = !string.IsNullOrWhiteSpace(path); + var hasSource = source is not null; + if (hasPath == hasSource) + { + Console.Error.WriteLine("Specify exactly one of --path/-p or --source/-s."); + return 1; + } + + var space = hasPath + ? FungeParser.ParseFile(path!) + : FungeParser.Parse(source!); var env = Environment.GetEnvironmentVariables() .Cast() .Select(static entry => $"{entry.Key}={entry.Value}"); + var inputArgument = hasPath ? path! : ""; var proc = new FungeProcessor( space, - commandLineArguments: [path], + commandLineArguments: [inputArgument], environmentVariables: env); return await proc.RunToConsoleAsync(cancellationToken); diff --git a/Interpreter/README.md b/Interpreter/README.md index dcde688..426816e 100644 --- a/Interpreter/README.md +++ b/Interpreter/README.md @@ -11,17 +11,30 @@ dotnet tool install -g dotnet-funge ## Usage ```bash -dotnet-funge +dotnet-funge --path ``` -| Argument | Description | +| Option | Description | | --- | --- | -| `` | Path to a Funge-98 source file (`.b98`) | +| `--path`, `-p` | Path to a Funge-98 source file (`.b98`) | +| `--source`, `-s` | Inline Funge-98 source code (supports newlines) | + +Specify exactly one of `--path` or `--source`. ### Example ```bash -dotnet-funge hello.b98 +dotnet-funge --path hello.b98 +``` + +### Inline Source Example (PowerShell) + +```powershell +$source = @' +v +>25*"!dlroW ,olleH",,,,@ +'@ +dotnet-funge --source $source ``` Standard input / output are connected to the running program (`~` / `&` for input, `,` / `.` for output). From 51a96799e534bbe6501902807fea23d010c7d6e8 Mon Sep 17 00:00:00 2001 From: juner Date: Wed, 3 Jun 2026 22:06:22 +0900 Subject: [PATCH 21/25] ci: align interpreter workflow with piet --- .github/workflows/dotnet.yml | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5bfc960..7f805fb 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -61,39 +61,48 @@ jobs: rm -rf .artifacts/tool-e2e mkdir -p .artifacts/tool-e2e - dotnet pack Interpreter/Esolang.Piet.Interpreter.csproj -r "$rid" -p:ToolPackageRuntimeIdentifiers= -o .artifacts/tool-e2e + dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -r "$rid" -p:ToolPackageRuntimeIdentifiers= -o .artifacts/tool-e2e - nupkg_path=$(find .artifacts/tool-e2e -maxdepth 1 -type f -name "dotnet-piet.*.nupkg" ! -name "*.snupkg" | head -n 1) + nupkg_path=$(find .artifacts/tool-e2e -maxdepth 1 -type f -name "dotnet-funge.*.nupkg" ! -name "*.snupkg" | head -n 1) if [ -z "$nupkg_path" ]; then - echo "Failed to find dotnet-piet nupkg in .artifacts/tool-e2e" + echo "Failed to find dotnet-funge nupkg in .artifacts/tool-e2e" exit 1 fi nupkg_name=$(basename "$nupkg_path") - tool_version=${nupkg_name#dotnet-piet.} + tool_version=${nupkg_name#dotnet-funge.} tool_version=${tool_version%.nupkg} echo "Detected tool version: $tool_version" dotnet new tool-manifest --force - dotnet tool install dotnet-piet \ + dotnet tool install dotnet-funge \ --add-source .artifacts/tool-e2e \ --version "$tool_version" - dotnet tool run dotnet-piet -- --help + dotnet tool run dotnet-funge -- --help # hello-world.png → "Hello, world!" を検証 - output1=$(dotnet tool run dotnet-piet -- samples/Generator.UseConsole/samples/hello-world.png) + output1=$(dotnet tool run dotnet-funge -- samples/Generator.UseConsole/samples/hello-world.png) echo "$output1" test "$output1" = "Hello, world!" # parse サブコマンドで ascii-piet テキストが出力されることを確認 - output2=$(dotnet tool run dotnet-piet -- parse samples/Generator.UseConsole/samples/hello-world.png) + output2=$(dotnet tool run dotnet-funge -- parse samples/Generator.UseConsole/samples/hello-world.png) echo "$output2" [ -n "$output2" ] # inline funge テキストで実行 - output3=$(dotnet tool run dotnet-funge -- --ascii-piet-text "l_ C") + output3=$(dotnet tool run dotnet-funge -- --source "64+\"!dlroW ,olleH\">:#,_@") echo "$output3" + # 改行を含む inline funge テキストで実行 + inline_source=$(cat <<'EOF' +v +>25*"!dlroW ,olleH",,,,@ +EOF +) + output4=$(dotnet tool run dotnet-funge -- --source "$inline_source") + echo "$output4" + dotnet tool uninstall dotnet-funge - name: Pack From 2dfa336166308c12d2cf6a66ab3a3ee54f561b35 Mon Sep 17 00:00:00 2001 From: juner Date: Wed, 3 Jun 2026 22:28:15 +0900 Subject: [PATCH 22/25] docs: align generator releases and changelog --- CHANGELOG.md | 11 ++++++++++- Generator/AnalyzerReleases.Shipped.md | 8 ++++++++ Generator/AnalyzerReleases.Unshipped.md | 3 +-- Generator/README.md | 5 +++-- README.md | 8 ++++---- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce5546..137dcb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ The format is based on Keep a Changelog. ## [Unreleased] ### Added + +## [2.0.0] - 2026-06-03 + +### Added + +- `Esolang.Funge.Interpreter`: added `--path` / `-p` for file input and `--source` / `-s` for inline Funge-98 source, including multiline source text. +- `.github/workflows/dotnet.yml`: updated the CI tool E2E step to exercise the interpreter's new inline-source execution path. +- `Esolang.Funge.Generator`: documented the argument-binding heuristics for `string[]` / `IEnumerable` parameters, including the `arg` / `env` name conventions used for `y`. - `Esolang.Funge.Generator`: Added FG0011 diagnostic for enforcing partial method declaration. ## [1.1.1] - 2026-05-25 @@ -96,7 +104,8 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Generator`: `FG0008` / `FG0009` severity changed from Warning to Info. - `Esolang.Funge.Generator`: runtime now throws when input/output instructions are executed without a declared input/output interface. -[Unreleased]: https://github.com/Esolang-NET/Funge/compare/v1.1.1...HEAD +[Unreleased]: https://github.com/Esolang-NET/Funge/compare/v2.0.0...HEAD +[2.0.0]: https://github.com/Esolang-NET/Funge/compare/v1.1.1...v2.0.0 [1.1.1]: https://github.com/Esolang-NET/Funge/tree/v1.1.1 [1.1.0]: https://github.com/Esolang-NET/Funge/tree/v1.1.0 [1.0.1]: https://github.com/Esolang-NET/Funge/tree/v1.0.1 diff --git a/Generator/AnalyzerReleases.Shipped.md b/Generator/AnalyzerReleases.Shipped.md index dc7074a..6b5e4a9 100644 --- a/Generator/AnalyzerReleases.Shipped.md +++ b/Generator/AnalyzerReleases.Shipped.md @@ -23,3 +23,11 @@ Rule ID | New Category | New Severity | Old Category | Old Severity | Notes --------|--------------|--------------|--------------|--------------|-------------------- FG0008 | Funge | Info | Funge | Warning | Static best-effort diagnostic; runtime throws if reached without output interface FG0009 | Funge | Info | Funge | Warning | Static best-effort diagnostic; runtime throws if reached without input interface + +## Release 2.0.0.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +FG0011 | Funge | Error | Method must be partial diff --git a/Generator/AnalyzerReleases.Unshipped.md b/Generator/AnalyzerReleases.Unshipped.md index 7c80f9f..3287c38 100644 --- a/Generator/AnalyzerReleases.Unshipped.md +++ b/Generator/AnalyzerReleases.Unshipped.md @@ -1,5 +1,4 @@ ### New Rules Rule ID | Category | Severity | Notes ---------|----------|----------|--------- -FG0011 | Funge | Error | Method must be partial.----------- +--------|----------|----------|--------- \ No newline at end of file diff --git a/Generator/README.md b/Generator/README.md index 96385f1..31e9de7 100644 --- a/Generator/README.md +++ b/Generator/README.md @@ -28,7 +28,7 @@ The generator reads the Funge-98 source (from a file or inline) and emits a comp | Parameter type | Role | | --- | --- | | `string` | Input fed to the program (`&` / `~`) | -| `string[]` or `IEnumerable` | Command-line arguments or environment variables (detected by name: `args`, `envs`, etc.). Reported by `y`. | +| `string[]` or `IEnumerable` | Command-line arguments or environment variables. The generator binds these by both type and name heuristics: parameter names containing `arg` map to command-line arguments, and names containing `env` map to environment variables. Reported by `y`. | | `System.IO.TextReader` | Input reader | | `System.IO.Pipelines.PipeReader` | Input as pipe | | `System.IO.TextWriter` | Explicit output sink for methods that do not return output text/bytes (including `void`, `int`, `Task`, `Task`, `ValueTask`, `ValueTask`) | @@ -64,7 +64,8 @@ partial class MyPrograms [GenerateFungeMethod("Programs/hello.b98")] public static partial string HelloWorld(); - // With custom arguments and environment variables (reported by 'y' instruction) + // With custom arguments and environment variables (reported by 'y' instruction). + // Parameter names matter here: names containing 'arg' bind as args, and names containing 'env' bind as envs. [GenerateFungeMethod("Programs/sysinfo.b98")] public static partial int RunWithArgs(string[] args, string[] envs); diff --git a/README.md b/README.md index adc7184..a052f13 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,10 @@ dotnet tool install -g dotnet-funge | Project | NuGet | Summary | |---|---|---| -| [dotnet-funge](./Interpreter/README.md) | [![NuGet: dotnet-funge](https://img.shields.io/nuget/v/dotnet-funge?logo=nuget&label=1.1.1)](https://www.nuget.org/packages/dotnet-funge/) | Funge-98 command-line interpreter. | -| [Esolang.Funge.Generator](./Generator/README.md) | [![NuGet: Esolang.Funge.Generator](https://img.shields.io/nuget/v/Esolang.Funge.Generator?logo=nuget&label=1.1.1)](https://www.nuget.org/packages/Esolang.Funge.Generator/) | Funge-98 source generator. | -| [Esolang.Funge.Parser](./Parser/README.md) | [![NuGet: Esolang.Funge.Parser](https://img.shields.io/nuget/v/Esolang.Funge.Parser?logo=nuget&label=1.1.1)](https://www.nuget.org/packages/Esolang.Funge.Parser/) | Funge-98 source parser. | -| [Esolang.Funge.Processor](./Processor/README.md) | [![NuGet: Esolang.Funge.Processor](https://img.shields.io/nuget/v/Esolang.Funge.Processor?logo=nuget&label=1.1.1)](https://www.nuget.org/packages/Esolang.Funge.Processor/) | Funge-98 execution engine. | +| [dotnet-funge](./Interpreter/README.md) | [![NuGet: dotnet-funge](https://img.shields.io/nuget/v/dotnet-funge?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/dotnet-funge/) | Funge-98 command-line interpreter. | +| [Esolang.Funge.Generator](./Generator/README.md) | [![NuGet: Esolang.Funge.Generator](https://img.shields.io/nuget/v/Esolang.Funge.Generator?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/Esolang.Funge.Generator/) | Funge-98 source generator. | +| [Esolang.Funge.Parser](./Parser/README.md) | [![NuGet: Esolang.Funge.Parser](https://img.shields.io/nuget/v/Esolang.Funge.Parser?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/Esolang.Funge.Parser/) | Funge-98 source parser. | +| [Esolang.Funge.Processor](./Processor/README.md) | [![NuGet: Esolang.Funge.Processor](https://img.shields.io/nuget/v/Esolang.Funge.Processor?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/Esolang.Funge.Processor/) | Funge-98 execution engine. | ## Framework Support From 2b71f606fb364e7fc2cb3ccf7211a722110803c7 Mon Sep 17 00:00:00 2001 From: juner Date: Wed, 3 Jun 2026 22:31:59 +0900 Subject: [PATCH 23/25] docs: remove redundant 'Added' section from unreleased changelog --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 137dcb4..594db47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,6 @@ The format is based on Keep a Changelog. ## [Unreleased] -### Added - ## [2.0.0] - 2026-06-03 ### Added From ad3031b21718ab4ffd7911adec5b355cc58bffc4 Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 4 Jun 2026 11:54:31 +0900 Subject: [PATCH 24/25] fix: correct Tool E2E tests (remove Piet-specific hello-world.png and parse subcommand) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dotnet.yml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 7f805fb..730a421 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -80,19 +80,15 @@ jobs: dotnet tool run dotnet-funge -- --help - # hello-world.png → "Hello, world!" を検証 - output1=$(dotnet tool run dotnet-funge -- samples/Generator.UseConsole/samples/hello-world.png) + # hello.b98 ファイルで実行 → "Hello, World!" を検証 + output1=$(dotnet tool run dotnet-funge -- --path samples/Generator.UseConsole/Programs/hello.b98) echo "$output1" - test "$output1" = "Hello, world!" - - # parse サブコマンドで ascii-piet テキストが出力されることを確認 - output2=$(dotnet tool run dotnet-funge -- parse samples/Generator.UseConsole/samples/hello-world.png) - echo "$output2" - [ -n "$output2" ] + test "$output1" = "Hello, World!" # inline funge テキストで実行 - output3=$(dotnet tool run dotnet-funge -- --source "64+\"!dlroW ,olleH\">:#,_@") - echo "$output3" + output2=$(dotnet tool run dotnet-funge -- --source "64+\"!dlroW ,olleH\">:#,_@") + echo "$output2" + test "$output2" = "Hello, World!" # 改行を含む inline funge テキストで実行 inline_source=$(cat <<'EOF' @@ -100,8 +96,8 @@ v >25*"!dlroW ,olleH",,,,@ EOF ) - output4=$(dotnet tool run dotnet-funge -- --source "$inline_source") - echo "$output4" + output3=$(dotnet tool run dotnet-funge -- --source "$inline_source") + echo "$output3" dotnet tool uninstall dotnet-funge From e20f36c3bf37ad5130c5669ee419675db42b28ae Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 4 Jun 2026 11:55:39 +0900 Subject: [PATCH 25/25] fix: restructure release.yml to AOT matrix pattern (align with Piet/Brainfuck) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 165 ++++++++++++++++++++++++++-------- 1 file changed, 129 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 829dbe1..8a0ae07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,33 +17,90 @@ permissions: env: DOTNET_VERSION: '10.0.x' - NUGET_SOURCE: 'https://api.nuget.org/v3/index.json' jobs: + build-aot: + name: build-aot-${{ matrix.rid }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: windows-latest + rid: win-x64 + - os: ubuntu-latest + rid: linux-x64 + - os: macos-latest + rid: osx-x64 + - os: macos-latest + rid: osx-arm64 + - os: ubuntu-latest + rid: any + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' && matrix.rid != 'any' + run: | + sudo apt-get update + sudo apt-get install -y clang zlib1g-dev + + - name: Pack + run: | + dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -r ${{ matrix.rid }} -c Release -o artifacts/ + + - name: Upload Artifact + uses: actions/upload-artifact@v7 + with: + name: nupkg-${{ matrix.rid }} + path: artifacts/* + publish: name: publish-packages-and-release + needs: build-aot runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} fetch-depth: 0 + - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download AOT Artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/aot + pattern: 'nupkg-*' + merge-multiple: true + - name: Restore - run: dotnet restore --source "${{ env.NUGET_SOURCE }}" + run: dotnet restore + - name: Build run: dotnet build --configuration Release --no-restore + - name: Test run: dotnet test --configuration Release --no-build --verbosity normal - - name: Pack + + - name: Pack Libraries run: | - dotnet pack Generator/Esolang.Funge.Generator.csproj -c Release -o artifacts/nuget - dotnet pack Parser/Esolang.Funge.Parser.csproj -c Release -o artifacts/nuget - dotnet pack Processor/Esolang.Funge.Processor.csproj -c Release -o artifacts/nuget - dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -c Release -o artifacts/nuget + dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -c Release -o artifacts/aot --no-build -p:ToolPackageRuntimeIdentifiers="" + dotnet pack Generator/Esolang.Funge.Generator.csproj -c Release -o artifacts/nuget --no-build + dotnet pack Parser/Esolang.Funge.Parser.csproj -c Release -o artifacts/nuget --no-build + dotnet pack Processor/Esolang.Funge.Processor.csproj -c Release -o artifacts/nuget --no-build + - name: Publish to NuGet.org env: NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} @@ -55,23 +112,48 @@ jobs: exit 1 fi shopt -s nullglob + push_nupkg() { + local pkg="$1" + dotnet nuget push "$pkg" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate + } + push_snupkg() { + local pkg="$1" + local snupkg="${pkg%.nupkg}.snupkg" + if [ -f "$snupkg" ]; then + dotnet nuget push "$snupkg" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate + fi + } + # Collect nupkg files in order: + # 1. RID-specific AOT packages (win-x64, linux-x64, osx-*, etc.) + # 2. any/portable package + # 3. no-RID meta package (the main dotnet-funge tool entry point) + # 4. Library packages (Generator, Parser, Processor) + ordered_nupkgs=() + for pkg in artifacts/aot/*.nupkg; do + [[ "$pkg" =~ \.(win|linux|osx|freebsd|android|ios)- ]] || continue + ordered_nupkgs+=("$pkg") + done + for pkg in artifacts/aot/*.nupkg; do + [[ "$pkg" == *".any."* ]] || continue + ordered_nupkgs+=("$pkg") + done + for pkg in artifacts/aot/*.nupkg; do + [[ "$pkg" =~ \.(win|linux|osx|freebsd|android|ios)- ]] && continue + [[ "$pkg" == *".any."* ]] && continue + ordered_nupkgs+=("$pkg") + done for pkg in artifacts/nuget/*.nupkg; do - case "$pkg" in - *.snupkg) - continue - ;; - esac - dotnet nuget push "$pkg" \ - --api-key "$NUGET_API_KEY" \ - --source https://api.nuget.org/v3/index.json \ - --skip-duplicate + ordered_nupkgs+=("$pkg") + done + # Upload all nupkg files first + for pkg in "${ordered_nupkgs[@]}"; do + push_nupkg "$pkg" done - for symbol_pkg in artifacts/nuget/*.snupkg; do - dotnet nuget push "$symbol_pkg" \ - --api-key "$NUGET_API_KEY" \ - --source https://api.nuget.org/v3/index.json \ - --skip-duplicate + # Then upload all snupkg files + for pkg in "${ordered_nupkgs[@]}"; do + push_snupkg "$pkg" done + - name: Publish to GitHub Packages env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -79,17 +161,13 @@ jobs: run: | set -euo pipefail shopt -s nullglob - for pkg in artifacts/nuget/*.nupkg; do + for pkg in artifacts/aot/*.nupkg artifacts/nuget/*.nupkg; do case "$pkg" in - *.snupkg) - continue - ;; + *.snupkg) continue ;; esac - dotnet nuget push "$pkg" \ - --api-key "$GITHUB_TOKEN" \ - --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ - --skip-duplicate + dotnet nuget push "$pkg" --api-key "$GITHUB_TOKEN" --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" --skip-duplicate done + - name: Create GitHub Release and Upload Assets env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -98,18 +176,33 @@ jobs: set -euo pipefail tag="${{ github.event.inputs.tag || github.ref_name }}" shopt -s nullglob - assets=(artifacts/nuget/*.nupkg artifacts/nuget/*.snupkg) + + lib_assets=(artifacts/nuget/*.nupkg artifacts/nuget/*.snupkg) + + aot_all=(artifacts/aot/*.nupkg artifacts/aot/*.snupkg) + aot_others=() + aot_any=() + for f in "${aot_all[@]}"; do + if [[ "$f" == *".any."* ]]; then + aot_any+=("$f") + else + aot_others+=("$f") + fi + done + + # Order: native RID packages → library packages → any/portable package + upload_files=("${aot_others[@]}" "${lib_assets[@]}" "${aot_any[@]}") + prerelease_flag="" if [[ "$tag" == *-* ]]; then prerelease_flag="--prerelease" fi + if gh release view "$tag" >/dev/null 2>&1; then - if [ ${#assets[@]} -gt 0 ]; then - gh release upload "$tag" "${assets[@]}" --clobber - fi + gh release upload "$tag" "${upload_files[@]}" --clobber else gh release create "$tag" \ - "${assets[@]}" \ + "${upload_files[@]}" \ --title "$tag" \ --generate-notes \ $prerelease_flag