From b0ecb798a377b469269da4cbc131085becc923b7 Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 7 May 2026 13:18:25 +0900 Subject: [PATCH 01/17] Add processor abstractions and RunToEnd integration --- CHANGELOG.md | 10 + Interpreter/FungeInterpreterExtensions.cs | 74 +- Processor.Tests/FungeProcessorTests.cs | 477 ++++---- Processor/Esolang.Funge.Processor.csproj | 4 + Processor/FungeProcessor.cs | 1253 +++++++++++---------- Processor/IProcessor.cs | 55 + 6 files changed, 992 insertions(+), 881 deletions(-) create mode 100644 Processor/IProcessor.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index bfe7ad4..2bb81d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on Keep a Changelog. ## [Unreleased] +### Added + +- `Esolang.Funge.Processor/Processor/IProcessor.cs`: provisional execution abstractions under `Esolang.Processor` (`IProcessor`, `ITextProcessor`, `IPipeProcessor`) for later extraction to a shared package. +- `Esolang.Funge.Processor.Tests`: coverage for `RunToEnd(...)` and `RunToEndAsync(...)` on `FungeProcessor`. + +### Changed + +- `Esolang.Funge.Processor`: `FungeProcessor` now implements `ITextProcessor` and exposes `RunToEnd(...)` / `RunToEndAsync(...)` while preserving existing `Run(...)` behavior. +- `dotnet-funge` (`Esolang.Funge.Interpreter`): command execution path now calls `RunToEnd(...)`. + ## [1.0.1] - 2026-05-07 ### Changed diff --git a/Interpreter/FungeInterpreterExtensions.cs b/Interpreter/FungeInterpreterExtensions.cs index da1f46e..917c6cd 100644 --- a/Interpreter/FungeInterpreterExtensions.cs +++ b/Interpreter/FungeInterpreterExtensions.cs @@ -1,37 +1,37 @@ -using Esolang.Funge.Parser; -using Esolang.Funge.Processor; -using System.CommandLine; - -namespace Esolang.Funge.Interpreter; - -/// -/// Extension methods that compose the dotnet-funge CLI commands. -/// -public static class FungeInterpreterExtensions -{ - /// - /// Builds and returns the root command for the dotnet-funge tool. - /// - public static RootCommand BuildRootCommand() - { - var pathArgument = new Argument("path") - { - Description = "Path to a Funge-98 source file (.b98).", - }; - - var rootCommand = new RootCommand("Run Funge-98 (Befunge-98) programs.") - { - pathArgument, - }; - - rootCommand.SetAction((parseResult, cancellationToken) => - { - var path = parseResult.GetValue(pathArgument)!; - var space = FungeParser.ParseFile(path); - var proc = new FungeProcessor(space, Console.Out, Console.In); - return Task.FromResult(proc.Run(cancellationToken)); - }); - - return rootCommand; - } -} +using Esolang.Funge.Parser; +using Esolang.Funge.Processor; +using System.CommandLine; + +namespace Esolang.Funge.Interpreter; + +/// +/// Extension methods that compose the dotnet-funge CLI commands. +/// +public static class FungeInterpreterExtensions +{ + /// + /// Builds and returns the root command for the dotnet-funge tool. + /// + public static RootCommand BuildRootCommand() + { + var pathArgument = new Argument("path") + { + Description = "Path to a Funge-98 source file (.b98).", + }; + + var rootCommand = new RootCommand("Run Funge-98 (Befunge-98) programs.") + { + pathArgument, + }; + + rootCommand.SetAction((parseResult, cancellationToken) => + { + var path = parseResult.GetValue(pathArgument)!; + var space = FungeParser.ParseFile(path); + var proc = new FungeProcessor(space, Console.Out, Console.In); + return Task.FromResult(proc.RunToEnd(cancellationToken: cancellationToken)); + }); + + return rootCommand; + } +} diff --git a/Processor.Tests/FungeProcessorTests.cs b/Processor.Tests/FungeProcessorTests.cs index ff52784..3959923 100644 --- a/Processor.Tests/FungeProcessorTests.cs +++ b/Processor.Tests/FungeProcessorTests.cs @@ -1,226 +1,251 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Esolang.Funge.Processor.Tests; - -[TestClass] -public class FungeProcessorTests -{ - public TestContext TestContext { get; set; } = default!; - - private 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(TestContext.CancellationTokenSource.Token); - return output.ToString(); - } - - private int RunGetExitCode(string source) - { - var space = Parser.FungeParser.Parse(source); - var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); - return proc.Run(TestContext.CancellationTokenSource.Token); - } - - // ── Termination ──────────────────────────────────────────────────────── - - [TestMethod] - [Timeout(5000)] - public void Stop_EmptyProgram_Wraps() - { - // No @ → program loops but should terminate via cancellation - // Just ensure an immediate @ exits - var result = Run("@"); - Assert.AreEqual(string.Empty, result); - } - - [TestMethod] - public void Quit_ReturnsExitCode() - => Assert.AreEqual(8, RunGetExitCode("42*q")); - - // ── Output ──────────────────────────────────────────────────────────── - - [TestMethod] - public void OutputChar_SingleChar() - => Assert.AreEqual("H", Run("\"H\",@")); - - [TestMethod] - public void OutputInt_WithTrailingSpace() - => Assert.AreEqual("10 ", Run("55+.@")); - - // ── Arithmetic ──────────────────────────────────────────────────────── - - [TestMethod] - public void Add() - => Assert.AreEqual("7 ", Run("34+.@")); - - [TestMethod] - public void Subtract() - => Assert.AreEqual("2 ", Run("53-.@")); - - [TestMethod] - public void Multiply() - => Assert.AreEqual("12 ", Run("34*.@")); - - [TestMethod] - public void Divide() - => Assert.AreEqual("1 ", Run("96/.@")); - - [TestMethod] - public void Remainder() - => Assert.AreEqual("1 ", Run("72%.@")); - - [TestMethod] - public void GreaterThan_True() - => Assert.AreEqual("1 ", Run("53`.@")); - - [TestMethod] - public void GreaterThan_False() - => Assert.AreEqual("0 ", Run("35`.@")); - - [TestMethod] - public void LogicalNot_Zero() - => Assert.AreEqual("1 ", Run("0!.@")); - - [TestMethod] - public void LogicalNot_NonZero() - => Assert.AreEqual("0 ", Run("5!.@")); - - // ── Stack ───────────────────────────────────────────────────────────── - - [TestMethod] - public void Duplicate() - => Assert.AreEqual("5 5 ", Run("5:..@")); - - [TestMethod] - public void Swap() - => Assert.AreEqual("5 3 ", Run("53\\..@")); - - [TestMethod] - public void Pop_Discard() - => Assert.AreEqual("5 ", Run("53$.@")); - - // ── Direction ───────────────────────────────────────────────────────── - - [TestMethod] -#pragma warning disable IDE0022 - public void EastWestIf_Zero_GoesEast() - { - // 0_ → East → . outputs next pop (0) then @ - Assert.AreEqual("0 ", Run("0_.@")); - } -#pragma warning restore IDE0022 - - [TestMethod] - [Timeout(5000)] -#pragma warning disable IDE0022 - public void NorthSouthIf_NonZero_GoesNorth() - { - const string source = "v @\n>1|"; - Assert.AreEqual(string.Empty, Run(source)); - } -#pragma warning restore IDE0022 - - [TestMethod] - [Timeout(5000)] -#pragma warning disable IDE0022 - public void EastWestIf_NonZero_GoesWest() - { - // "1_" at positions 0-1. After '_', go West, wrap to rightmost char... - // Hard to test in single row. Use '@' placement. - // "1_@" → goes West to nothing... let's try another approach - // Just verify we can stop: if nonzero, go West; space wraps; '@' at start doesn't help - // Skip complex direction tests here; covered by Hello World test below - Assert.AreEqual(string.Empty, Run("1_@")); // goes West, wraps, hits '_' etc. – eventually '@' or loops - } -#pragma warning restore IDE0022 - - // ── Hex digits ──────────────────────────────────────────────────────── - - [TestMethod] - public void HexDigits() - => Assert.AreEqual("15 14 13 12 11 10 ", Run("abcdef......@")); - - // ── String mode ─────────────────────────────────────────────────────── - - [TestMethod] -#pragma warning disable IDE0022 - public void StringMode_PushesChars() - { - // "Hi" pushes 'H'=72 then 'i'=105; i is on top - Assert.AreEqual("iH", Run("\"Hi\",,@")); - } -#pragma warning restore IDE0022 - - [TestMethod] - public void StringMode_ContiguousSpaces_PushSingleSpace() - => Assert.AreEqual("49 ", Run("\" 1\".,@")); - - // ── Trampoline ──────────────────────────────────────────────────────── - - [TestMethod] - [Timeout(5000)] -#pragma warning disable IDE0022 - public void Trampoline_SkipsOne() - { - // "#.@" → skip '.', execute '@' → empty output - Assert.AreEqual(string.Empty, Run("#.@")); - } -#pragma warning restore IDE0022 - - [TestMethod] - public void SgmlSpaces_DoNotReflect() - => Assert.AreEqual("1 ", Run("1\t\f\v.@")); - - // ── FungeSpace get/put ──────────────────────────────────────────────── - - [TestMethod] -#pragma warning disable IDE0022 - public void GetPut_ReadWrite() - { - // p pops y,x,v. Build v=65 via 8*8+1, then store at (5,0) and read back. - Assert.AreEqual("65 ", Run("88*1+50p50g.@")); - } -#pragma warning restore IDE0022 - - // ── Hello World ─────────────────────────────────────────────────────── - - [TestMethod] - [Timeout(5000)] -#pragma warning disable IDE0022 - public void HelloWorld_Classic() - { - // Classic Befunge-98 Hello World (one-liner) - const string src = "\"olleH\">:#,_@"; - Assert.AreEqual("Hello", Run(src)); - } -#pragma warning restore IDE0022 - - [TestMethod] - [Timeout(5000)] -#pragma warning disable IDE0022 - public void HelloWorld_WithExclamation() - { - const string src = "\"!dlroW ,olleH\">:#,_@"; - Assert.AreEqual("Hello, World!", Run(src)); - } -#pragma warning restore IDE0022 - - // ── Input ───────────────────────────────────────────────────────────── - - [TestMethod] - public void InputChar_EchoBack() - => Assert.AreEqual("A", Run("~,@", "A")); - - [TestMethod] - public void InputInt_EchoBack() - => Assert.AreEqual("42 ", Run("&.@", "42\n")); - - // ── Quit exit code ──────────────────────────────────────────────────── - - [TestMethod] - public void Quit_ExitCode7() - => Assert.AreEqual(7, RunGetExitCode("7q")); -} +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Esolang.Funge.Processor.Tests; + +[TestClass] +public class FungeProcessorTests +{ + public TestContext TestContext { get; set; } = default!; + + private 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(TestContext.CancellationTokenSource.Token); + return output.ToString(); + } + + private int RunGetExitCode(string source) + { + var space = Parser.FungeParser.Parse(source); + var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); + return proc.Run(TestContext.CancellationTokenSource.Token); + } + + // ── Termination ──────────────────────────────────────────────────────── + + [TestMethod] + [Timeout(5000)] + public void Stop_EmptyProgram_Wraps() + { + // No @ → program loops but should terminate via cancellation + // Just ensure an immediate @ exits + var result = Run("@"); + Assert.AreEqual(string.Empty, result); + } + + [TestMethod] + public void Quit_ReturnsExitCode() + => Assert.AreEqual(8, RunGetExitCode("42*q")); + + // ── Output ──────────────────────────────────────────────────────────── + + [TestMethod] + public void OutputChar_SingleChar() + => Assert.AreEqual("H", Run("\"H\",@")); + + [TestMethod] + public void OutputInt_WithTrailingSpace() + => Assert.AreEqual("10 ", Run("55+.@")); + + // ── Arithmetic ──────────────────────────────────────────────────────── + + [TestMethod] + public void Add() + => Assert.AreEqual("7 ", Run("34+.@")); + + [TestMethod] + public void Subtract() + => Assert.AreEqual("2 ", Run("53-.@")); + + [TestMethod] + public void Multiply() + => Assert.AreEqual("12 ", Run("34*.@")); + + [TestMethod] + public void Divide() + => Assert.AreEqual("1 ", Run("96/.@")); + + [TestMethod] + public void Remainder() + => Assert.AreEqual("1 ", Run("72%.@")); + + [TestMethod] + public void GreaterThan_True() + => Assert.AreEqual("1 ", Run("53`.@")); + + [TestMethod] + public void GreaterThan_False() + => Assert.AreEqual("0 ", Run("35`.@")); + + [TestMethod] + public void LogicalNot_Zero() + => Assert.AreEqual("1 ", Run("0!.@")); + + [TestMethod] + public void LogicalNot_NonZero() + => Assert.AreEqual("0 ", Run("5!.@")); + + // ── Stack ───────────────────────────────────────────────────────────── + + [TestMethod] + public void Duplicate() + => Assert.AreEqual("5 5 ", Run("5:..@")); + + [TestMethod] + public void Swap() + => Assert.AreEqual("5 3 ", Run("53\\..@")); + + [TestMethod] + public void Pop_Discard() + => Assert.AreEqual("5 ", Run("53$.@")); + + // ── Direction ───────────────────────────────────────────────────────── + + [TestMethod] +#pragma warning disable IDE0022 + public void EastWestIf_Zero_GoesEast() + { + // 0_ → East → . outputs next pop (0) then @ + Assert.AreEqual("0 ", Run("0_.@")); + } +#pragma warning restore IDE0022 + + [TestMethod] + [Timeout(5000)] +#pragma warning disable IDE0022 + public void NorthSouthIf_NonZero_GoesNorth() + { + const string source = "v @\n>1|"; + Assert.AreEqual(string.Empty, Run(source)); + } +#pragma warning restore IDE0022 + + [TestMethod] + [Timeout(5000)] +#pragma warning disable IDE0022 + public void EastWestIf_NonZero_GoesWest() + { + // "1_" at positions 0-1. After '_', go West, wrap to rightmost char... + // Hard to test in single row. Use '@' placement. + // "1_@" → goes West to nothing... let's try another approach + // Just verify we can stop: if nonzero, go West; space wraps; '@' at start doesn't help + // Skip complex direction tests here; covered by Hello World test below + Assert.AreEqual(string.Empty, Run("1_@")); // goes West, wraps, hits '_' etc. – eventually '@' or loops + } +#pragma warning restore IDE0022 + + // ── Hex digits ──────────────────────────────────────────────────────── + + [TestMethod] + public void HexDigits() + => Assert.AreEqual("15 14 13 12 11 10 ", Run("abcdef......@")); + + // ── String mode ─────────────────────────────────────────────────────── + + [TestMethod] +#pragma warning disable IDE0022 + public void StringMode_PushesChars() + { + // "Hi" pushes 'H'=72 then 'i'=105; i is on top + Assert.AreEqual("iH", Run("\"Hi\",,@")); + } +#pragma warning restore IDE0022 + + [TestMethod] + public void StringMode_ContiguousSpaces_PushSingleSpace() + => Assert.AreEqual("49 ", Run("\" 1\".,@")); + + // ── Trampoline ──────────────────────────────────────────────────────── + + [TestMethod] + [Timeout(5000)] +#pragma warning disable IDE0022 + public void Trampoline_SkipsOne() + { + // "#.@" → skip '.', execute '@' → empty output + Assert.AreEqual(string.Empty, Run("#.@")); + } +#pragma warning restore IDE0022 + + [TestMethod] + public void SgmlSpaces_DoNotReflect() + => Assert.AreEqual("1 ", Run("1\t\f\v.@")); + + // ── FungeSpace get/put ──────────────────────────────────────────────── + + [TestMethod] +#pragma warning disable IDE0022 + public void GetPut_ReadWrite() + { + // p pops y,x,v. Build v=65 via 8*8+1, then store at (5,0) and read back. + Assert.AreEqual("65 ", Run("88*1+50p50g.@")); + } +#pragma warning restore IDE0022 + + // ── Hello World ─────────────────────────────────────────────────────── + + [TestMethod] + [Timeout(5000)] +#pragma warning disable IDE0022 + public void HelloWorld_Classic() + { + // Classic Befunge-98 Hello World (one-liner) + const string src = "\"olleH\">:#,_@"; + Assert.AreEqual("Hello", Run(src)); + } +#pragma warning restore IDE0022 + + [TestMethod] + [Timeout(5000)] +#pragma warning disable IDE0022 + public void HelloWorld_WithExclamation() + { + const string src = "\"!dlroW ,olleH\">:#,_@"; + Assert.AreEqual("Hello, World!", Run(src)); + } +#pragma warning restore IDE0022 + + // ── Input ───────────────────────────────────────────────────────────── + + [TestMethod] + public void InputChar_EchoBack() + => Assert.AreEqual("A", Run("~,@", "A")); + + [TestMethod] + public void InputInt_EchoBack() + => Assert.AreEqual("42 ", Run("&.@", "42\n")); + + // ── Quit exit code ──────────────────────────────────────────────────── + + [TestMethod] + public void Quit_ExitCode7() + => Assert.AreEqual(7, RunGetExitCode("7q")); + + [TestMethod] + 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 exitCode = proc.RunToEnd(input, output, TestContext.CancellationTokenSource.Token); + + Assert.AreEqual(0, exitCode); + Assert.AreEqual("42 ", output.ToString()); + } + + [TestMethod] + public async Task RunToEndAsync_ReturnsExitCode() + { + var space = Parser.FungeParser.Parse("7q"); + var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); + + var exitCode = await proc.RunToEndAsync(cancellationToken: TestContext.CancellationTokenSource.Token); + + Assert.AreEqual(7, exitCode); + } +} diff --git a/Processor/Esolang.Funge.Processor.csproj b/Processor/Esolang.Funge.Processor.csproj index 27e025f..1177d80 100644 --- a/Processor/Esolang.Funge.Processor.csproj +++ b/Processor/Esolang.Funge.Processor.csproj @@ -20,6 +20,10 @@ false + + + + diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index bddd335..1dd6a90 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -1,618 +1,635 @@ -using Esolang.Funge.Parser; - -namespace Esolang.Funge.Processor; - -/// -/// Executes a Funge-98 program loaded into a . -/// Supports the full core instruction set including concurrent IPs (t) and -/// the stack stack ({/}/u). -/// Fingerprints ((/)) and file I/O (i/o) reflect (not implemented). -/// 3-D instructions (h/l/m) reflect in 2-D mode. -/// -public sealed class FungeProcessor -{ - private readonly FungeSpace _space; - private readonly TextWriter _output; - private readonly TextReader _input; - private readonly Random _random = new(); - private int _nextIpId; - - /// - /// 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 . - public FungeProcessor(FungeSpace space, TextWriter? output = null, TextReader? input = null) - { - _space = space; - _output = output ?? Console.Out; - _input = input ?? Console.In; - } - - /// - /// 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) - { - 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); - - if (ip.IsStopped || quit) - { - ips.Remove(node); - } - else if (!suppressAdvance) - { - ip.Position = _space.Advance(ip.Position, ip.Delta); - } - - node = nextNode; - } - } - - return exitCode; - } - - private void ExecuteInstruction( - InstructionPointer ip, - LinkedList ips, - LinkedListNode ipNode, - ref int exitCode, - ref bool quit, - ref bool suppressAdvance, - int? overrideCell = null) - { - var cell = overrideCell ?? _space[ip.Position]; - - // String mode: push each character until closing " - if (ip.StringMode) - { - if (cell == '"') - { - ip.StringMode = false; - } - else if (cell is ' ' or '\t' or '\f' or '\v') - { - // Funge-98 stringmode treats contiguous spaces SGML-style: - // one pushed space, one tick. - ip.StackStack.Push(' '); - while (true) - { - var next = _space.Advance(ip.Position, ip.Delta); - var nextCell = _space[next]; - if (nextCell is ' ' or '\t' or '\f' or '\v') - { - ip.Position = next; - } - else - { - break; - } - } - } - else - { - ip.StackStack.Push(cell); - } - return; - } - - switch (cell) - { - // ── No-ops ────────────────────────────────────────────────────── - case ' ': // Space: no-op (IP passes through) - case '\t': // SGML space: tab - case '\f': // SGML space: form feed - case '\v': // SGML space: vertical tab - case 'z': // z: explicit no-op - break; - - // ── Stack manipulation ─────────────────────────────────────────── - case '!': // Logical Not - ip.StackStack.Push(ip.StackStack.Pop() == 0 ? 1 : 0); - break; - - case '$': // Pop - ip.StackStack.Pop(); - break; - - case ':': // Duplicate - { - var v = ip.StackStack.Pop(); - ip.StackStack.Push(v); - ip.StackStack.Push(v); - break; - } - - case '\\': // Swap - { - int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); - ip.StackStack.Push(b); - ip.StackStack.Push(a); - break; - } - - case 'n': // Clear Stack - ip.StackStack.ClearToss(); - break; - - // ── Arithmetic ─────────────────────────────────────────────────── - case '+': - { - int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); - ip.StackStack.Push(a + b); - break; - } - - case '-': - { - int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); - ip.StackStack.Push(a - b); - break; - } - - case '*': - { - int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); - ip.StackStack.Push(a * b); - break; - } - - case '/': - { - int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); - ip.StackStack.Push(b == 0 ? 0 : a / b); - break; - } - - case '%': // Remainder - { - int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); - ip.StackStack.Push(b == 0 ? 0 : a % b); - break; - } - - case '`': // Greater Than - { - int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); - ip.StackStack.Push(a > b ? 1 : 0); - break; - } - - // ── Digit/hex pushers ──────────────────────────────────────────── - case '0' or '1' or '2' or '3' or '4' - or '5' or '6' or '7' or '8' or '9': - ip.StackStack.Push(cell - '0'); - break; - - case 'a': ip.StackStack.Push(10); break; - case 'b': ip.StackStack.Push(11); break; - case 'c': ip.StackStack.Push(12); break; - case 'd': ip.StackStack.Push(13); break; - case 'e': ip.StackStack.Push(14); break; - case 'f': ip.StackStack.Push(15); break; - - // ── Direction ─────────────────────────────────────────────────── - case '>': ip.Delta = FungeVector.East; break; - case '<': ip.Delta = FungeVector.West; break; - case '^': ip.Delta = FungeVector.North; break; - case 'v': ip.Delta = FungeVector.South; break; - - case '?': // Go Away: random cardinal direction - ip.Delta = _random.Next(4) switch - { - 0 => FungeVector.East, - 1 => FungeVector.West, - 2 => FungeVector.North, - _ => FungeVector.South, - }; - break; - - case '_': // East-West If - ip.Delta = ip.StackStack.Pop() == 0 ? FungeVector.East : FungeVector.West; - break; - - case '|': // North-South If - ip.Delta = ip.StackStack.Pop() == 0 ? FungeVector.South : FungeVector.North; - break; - - case '[': // Turn Left (CCW 90°) - ip.Delta = ip.Delta.RotateLeft(); - break; - - case ']': // Turn Right (CW 90°) - ip.Delta = ip.Delta.RotateRight(); - break; - - case 'r': // Reflect - ip.Delta = ip.Delta.Reflect(); - break; - - case 'x': // Absolute Delta - { - int dy = ip.StackStack.Pop(), dx = ip.StackStack.Pop(); - ip.Delta = new FungeVector(dx, dy); - break; - } - - case 'w': // Compare - { - int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); - if (a > b) ip.Delta = ip.Delta.RotateRight(); - else if (a < b) ip.Delta = ip.Delta.RotateLeft(); - // a == b: no change (acts as 'z') - break; - } - - // ── Movement modifiers ─────────────────────────────────────────── - case '#': // Trampoline: skip next cell - ip.Position = _space.Advance(ip.Position, ip.Delta); - break; - - case 'j': // Jump Forward s cells (suppressAdvance: sets position directly) - { - var s = ip.StackStack.Pop(); - 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; - break; - } - - case ';': // Jump Over: skip until next ; - ip.Position = _space.Advance(ip.Position, ip.Delta); - while (_space[ip.Position] != ';') - ip.Position = _space.Advance(ip.Position, ip.Delta); - break; - - // ── Character fetch/store ──────────────────────────────────────── - case '\'': // Fetch Character: push value of next cell, skip it - ip.Position = _space.Advance(ip.Position, ip.Delta); - ip.StackStack.Push(_space[ip.Position]); - break; - - case 's': // Store Character: store to next cell, skip it - { - var val = ip.StackStack.Pop(); - ip.Position = _space.Advance(ip.Position, ip.Delta); - _space[ip.Position] = val; - break; - } - - // ── String mode ────────────────────────────────────────────────── - case '"': // Toggle Stringmode - ip.StringMode = true; - break; - - // ── FungeSpace get/put ─────────────────────────────────────────── - case 'g': // Get: read cell at (x+offset, y+offset) - { - int y = ip.StackStack.Pop(), x = ip.StackStack.Pop(); - ip.StackStack.Push(_space[new FungeVector(x + ip.Offset.X, y + ip.Offset.Y)]); - break; - } - - case 'p': // Put: write cell at (x+offset, y+offset) - { - int y = ip.StackStack.Pop(), x = ip.StackStack.Pop(); - var val = ip.StackStack.Pop(); - _space[new FungeVector(x + ip.Offset.X, y + ip.Offset.Y)] = val; - break; - } - - // ── I/O ────────────────────────────────────────────────────────── - case '.': // Output Integer - _output.Write(ip.StackStack.Pop()); - _output.Write(' '); - break; - - case ',': // Output Character - _output.Write((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); - break; - } - - case '~': // Input Character - { - var ch = _input.Read(); - if (ch < 0) ip.Delta = ip.Delta.Reflect(); - else ip.StackStack.Push(ch); - break; - } - - // ── Control flow ───────────────────────────────────────────────── - case '@': // Stop this IP - ip.IsStopped = true; - break; - - case 'q': // Quit program immediately - exitCode = ip.StackStack.Pop(); - quit = true; - break; - - case 'k': // Iterate: execute next instruction n times - { - var n = ip.StackStack.Pop(); - - // Advance past spaces AND semicolon-delimited sections to find the operand. - // Per spec, k skips spaces and ';'-enclosed regions just as normal execution would. - var instrPos = _space.Advance(ip.Position, ip.Delta); - while (true) - { - var c = _space[instrPos]; - if (c is ' ' or '\t' or '\f' or '\v') - { - instrPos = _space.Advance(instrPos, ip.Delta); - } - else if (c == ';') - { - // skip the semicolon section entirely (same as the ';' instruction) - instrPos = _space.Advance(instrPos, ip.Delta); - while (_space[instrPos] != ';') - instrPos = _space.Advance(instrPos, ip.Delta); - instrPos = _space.Advance(instrPos, ip.Delta); - } - else - { - break; - } - } - - if (n == 0) - { - // n=0: skip the operand. IP moves to instrPos, then normal advance passes it. - ip.Position = instrPos; - } - else - { - // n>0: execute the discovered operand n times, but execute it AT k. - // The operand is only searched for; its semantics apply at the current IP position. - // 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++) - { - var dummy = false; - ExecuteInstruction(ip, ips, ipNode, ref exitCode, ref quit, ref dummy, operand); - } - } - break; - } - - // ── Concurrency ────────────────────────────────────────────────── - case 't': // Split: create child IP with reflected delta - { - var child = ip.CreateChild(_nextIpId++); - ips.AddAfter(ipNode, child); - break; - } - - // ── Stack Stack operations ──────────────────────────────────────── - case '{': // Begin Block - { - var n = ip.StackStack.Pop(); - - // Collect n items from TOSS (top item first) - var items = new List(); - if (n > 0) - for (var i = 0; i < n; i++) items.Add(ip.StackStack.Pop()); - - // Push storage offset to current TOSS (will become SOSS) - ip.StackStack.Push(ip.Offset.X); - ip.StackStack.Push(ip.Offset.Y); - - // Push new empty stack (old TOSS becomes SOSS) - ip.StackStack.PushNewStack(); - - if (n > 0) - { - // Re-push items so original top is on top of new TOSS - for (var i = items.Count - 1; i >= 0; i--) - ip.StackStack.Push(items[i]); - } - else if (n < 0) - { - // Push |n| zeros to SOSS - var soss = ip.StackStack.SOSS!; - for (var i = 0; i < -n; i++) soss.Push(0); - } - - // Set storage offset to next cell position - ip.Offset = _space.Advance(ip.Position, ip.Delta); - break; - } - - case '}': // End Block - { - var n = ip.StackStack.Pop(); - if (!ip.StackStack.HasSOSS) - { - ip.Delta = ip.Delta.Reflect(); - break; - } - - // Collect items from TOSS - var items = new List(); - for (var i = 0; i < Math.Max(0, n); i++) items.Add(ip.StackStack.Pop()); - - // Pop current TOSS (discard remaining items) - ip.StackStack.PopCurrentStack(); - - // Restore storage offset (Y on top, then X) - var oy = ip.StackStack.Pop(); - var ox = ip.StackStack.Pop(); - ip.Offset = new FungeVector(ox, oy); - - // If n < 0, discard |n| items from (now current) TOSS - if (n < 0) - for (var i = 0; i < -n; i++) ip.StackStack.Pop(); - - // Push collected items (original top on top) - for (var i = items.Count - 1; i >= 0; i--) - ip.StackStack.Push(items[i]); - break; - } - - case 'u': // Stack Under Stack - { - var n = ip.StackStack.Pop(); - if (!ip.StackStack.HasSOSS) - { - ip.Delta = ip.Delta.Reflect(); - break; - } - var soss = ip.StackStack.SOSS!; - if (n > 0) - for (var i = 0; i < n; i++) ip.StackStack.Push(soss.Count > 0 ? soss.Pop() : 0); - else if (n < 0) - for (var i = 0; i < -n; i++) soss.Push(ip.StackStack.Pop()); - break; - } - - // ── System info ────────────────────────────────────────────────── - case 'y': // Get SysInfo - { - var c = ip.StackStack.Pop(); - PushSysInfo(ip, ips.Count, c); - break; - } - - // ── Fingerprints (reflect – not implemented) ───────────────────── - case '(': // Load Semantics - { - var n = ip.StackStack.Pop(); - for (var i = 0; i < n; i++) ip.StackStack.Pop(); - ip.Delta = ip.Delta.Reflect(); - break; - } - - case ')': // Unload Semantics - { - var n = ip.StackStack.Pop(); - for (var i = 0; i < n; i++) ip.StackStack.Pop(); - ip.Delta = ip.Delta.Reflect(); - break; - } - - // ── Optional / 3-D-only (reflect) ──────────────────────────────── - case '=': // Execute (system exec) – reflect - { - // Consume 0gnirts command string from stack - while (ip.StackStack.Pop() != 0) { } - ip.Delta = ip.Delta.Reflect(); - break; - } - - case 'i': // Input File – reflect - case 'o': // Output File – reflect - case 'h': // Go High (3-D) – reflect - case 'l': // Go Low (3-D) – reflect - case 'm': // High-Low If (3-D) – reflect - ip.Delta = ip.Delta.Reflect(); - break; - - default: - // A-Z: fingerprint-defined; reflect if not loaded - if (cell is >= 'A' and <= 'Z') - ip.Delta = ip.Delta.Reflect(); - // All other characters: no-op - break; - } - } - - /// - /// Pushes system information onto the TOSS of the given IP. - /// If is greater than zero, only item - /// (1-indexed from top) is left on the stack. - /// - private 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 = []; - - // 1. Flags: bit 0 = /t (concurrency supported) - items.Add(1); - // 2. Cell size in bytes - items.Add(4); - // 3. Interpreter handprint ("Fung" as big-endian int) - items.Add(unchecked((int)0x46756E67u)); - // 4. Version (Funge-98 = 9800) - items.Add(9800); - // 5. Operating paradigm (0 = system() unavailable) - items.Add(0); - // 6. Path separator - items.Add(Path.DirectorySeparatorChar); - // 7. Number of dimensions (2 = Befunge) - items.Add(2); - // 8. IP unique ID - items.Add(ip.Id); - // 9. IP team number - items.Add(0); - // 10-11. IP position (X, Y; Y on top) - items.Add(ip.Position.X); - items.Add(ip.Position.Y); - // 12-13. IP delta (dX, dY; dY on top) - items.Add(ip.Delta.X); - items.Add(ip.Delta.Y); - // 14-15. Storage offset (oX, oY; oY on top) - items.Add(ip.Offset.X); - items.Add(ip.Offset.Y); - // 16-17. Least point of LSAB (minX, minY; minY on top) - items.Add(_space.MinX); - items.Add(_space.MinY); - // 18-19. Greatest point of LSAB (maxX, maxY; maxY on top) - items.Add(_space.MaxX); - items.Add(_space.MaxY); - - var now = DateTime.Now; - // 20. Current date: (year-1900)*10000 + month*100 + day - items.Add(((now.Year - 1900) * 10000) + (now.Month * 100) + now.Day); - // 21. Current time: HH*10000 + MM*100 + SS - items.Add((now.Hour * 10000) + (now.Minute * 100) + now.Second); - // 22. Number of stacks in stack stack - items.Add(ip.StackStack.StackCount); - // 23+. Size of each stack (TOSS first) - foreach (var stack in ip.StackStack.AllStacks) - items.Add(stack.Count); - // Command-line args: empty list (single 0 terminator) - items.Add(0); - // Environment variables: empty list (single 0 terminator) - items.Add(0); - - // Push in reverse order so items[0] ends up on top (= item 1) - for (var i = items.Count - 1; i >= 0; i--) - ip.StackStack.Push(items[i]); - - if (c > 0) - { - // Pick the c-th item from top of pushed items - var popped = new int[items.Count]; - for (var i = 0; i < items.Count; i++) - popped[i] = ip.StackStack.Pop(); - ip.StackStack.Push(c <= items.Count ? popped[c - 1] : 0); - } - } -} +using Esolang.Funge.Parser; +using Esolang.Processor; + +namespace Esolang.Funge.Processor; + +/// +/// Executes a Funge-98 program loaded into a . +/// Supports the full core instruction set including concurrent IPs (t) and +/// the stack stack ({/}/u). +/// Fingerprints ((/)) and file I/O (i/o) reflect (not implemented). +/// 3-D instructions (h/l/m) reflect in 2-D mode. +/// +public sealed partial class FungeProcessor : ITextProcessor +{ + private readonly FungeSpace _space; + private readonly TextWriter _output; + private readonly TextReader _input; + private readonly Random _random = new(); + private int _nextIpId; + + /// + public FungeSpace Program => _space; + + /// + /// 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 . + public FungeProcessor(FungeSpace space, TextWriter? output = null, TextReader? input = null) + { + _space = space; + _output = output ?? Console.Out; + _input = input ?? Console.In; + } + + /// + /// 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) + { + 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; + } + + /// + public ValueTask RunToEndAsync(TextReader? input = null, TextWriter? output = null, CancellationToken cancellationToken = default) + => ValueTask.FromResult(RunToEnd(input, output, cancellationToken)); + + private void ExecuteInstruction( + InstructionPointer ip, + LinkedList ips, + LinkedListNode ipNode, + ref int exitCode, + ref bool quit, + ref bool suppressAdvance, + TextReader input, + TextWriter output, + int? overrideCell = null) + { + var cell = overrideCell ?? _space[ip.Position]; + + // String mode: push each character until closing " + if (ip.StringMode) + { + if (cell == '"') + { + ip.StringMode = false; + } + else if (cell is ' ' or '\t' or '\f' or '\v') + { + // Funge-98 stringmode treats contiguous spaces SGML-style: + // one pushed space, one tick. + ip.StackStack.Push(' '); + while (true) + { + var next = _space.Advance(ip.Position, ip.Delta); + var nextCell = _space[next]; + if (nextCell is ' ' or '\t' or '\f' or '\v') + { + ip.Position = next; + } + else + { + break; + } + } + } + else + { + ip.StackStack.Push(cell); + } + return; + } + + switch (cell) + { + // ── No-ops ────────────────────────────────────────────────────── + case ' ': // Space: no-op (IP passes through) + case '\t': // SGML space: tab + case '\f': // SGML space: form feed + case '\v': // SGML space: vertical tab + case 'z': // z: explicit no-op + break; + + // ── Stack manipulation ─────────────────────────────────────────── + case '!': // Logical Not + ip.StackStack.Push(ip.StackStack.Pop() == 0 ? 1 : 0); + break; + + case '$': // Pop + ip.StackStack.Pop(); + break; + + case ':': // Duplicate + { + var v = ip.StackStack.Pop(); + ip.StackStack.Push(v); + ip.StackStack.Push(v); + break; + } + + case '\\': // Swap + { + int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); + ip.StackStack.Push(b); + ip.StackStack.Push(a); + break; + } + + case 'n': // Clear Stack + ip.StackStack.ClearToss(); + break; + + // ── Arithmetic ─────────────────────────────────────────────────── + case '+': + { + int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); + ip.StackStack.Push(a + b); + break; + } + + case '-': + { + int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); + ip.StackStack.Push(a - b); + break; + } + + case '*': + { + int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); + ip.StackStack.Push(a * b); + break; + } + + case '/': + { + int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); + ip.StackStack.Push(b == 0 ? 0 : a / b); + break; + } + + case '%': // Remainder + { + int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); + ip.StackStack.Push(b == 0 ? 0 : a % b); + break; + } + + case '`': // Greater Than + { + int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); + ip.StackStack.Push(a > b ? 1 : 0); + break; + } + + // ── Digit/hex pushers ──────────────────────────────────────────── + case '0' or '1' or '2' or '3' or '4' + or '5' or '6' or '7' or '8' or '9': + ip.StackStack.Push(cell - '0'); + break; + + case 'a': ip.StackStack.Push(10); break; + case 'b': ip.StackStack.Push(11); break; + case 'c': ip.StackStack.Push(12); break; + case 'd': ip.StackStack.Push(13); break; + case 'e': ip.StackStack.Push(14); break; + case 'f': ip.StackStack.Push(15); break; + + // ── Direction ─────────────────────────────────────────────────── + case '>': ip.Delta = FungeVector.East; break; + case '<': ip.Delta = FungeVector.West; break; + case '^': ip.Delta = FungeVector.North; break; + case 'v': ip.Delta = FungeVector.South; break; + + case '?': // Go Away: random cardinal direction + ip.Delta = _random.Next(4) switch + { + 0 => FungeVector.East, + 1 => FungeVector.West, + 2 => FungeVector.North, + _ => FungeVector.South, + }; + break; + + case '_': // East-West If + ip.Delta = ip.StackStack.Pop() == 0 ? FungeVector.East : FungeVector.West; + break; + + case '|': // North-South If + ip.Delta = ip.StackStack.Pop() == 0 ? FungeVector.South : FungeVector.North; + break; + + case '[': // Turn Left (CCW 90°) + ip.Delta = ip.Delta.RotateLeft(); + break; + + case ']': // Turn Right (CW 90°) + ip.Delta = ip.Delta.RotateRight(); + break; + + case 'r': // Reflect + ip.Delta = ip.Delta.Reflect(); + break; + + case 'x': // Absolute Delta + { + int dy = ip.StackStack.Pop(), dx = ip.StackStack.Pop(); + ip.Delta = new FungeVector(dx, dy); + break; + } + + case 'w': // Compare + { + int b = ip.StackStack.Pop(), a = ip.StackStack.Pop(); + if (a > b) ip.Delta = ip.Delta.RotateRight(); + else if (a < b) ip.Delta = ip.Delta.RotateLeft(); + // a == b: no change (acts as 'z') + break; + } + + // ── Movement modifiers ─────────────────────────────────────────── + case '#': // Trampoline: skip next cell + ip.Position = _space.Advance(ip.Position, ip.Delta); + break; + + case 'j': // Jump Forward s cells (suppressAdvance: sets position directly) + { + var s = ip.StackStack.Pop(); + 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; + break; + } + + case ';': // Jump Over: skip until next ; + ip.Position = _space.Advance(ip.Position, ip.Delta); + while (_space[ip.Position] != ';') + ip.Position = _space.Advance(ip.Position, ip.Delta); + break; + + // ── Character fetch/store ──────────────────────────────────────── + case '\'': // Fetch Character: push value of next cell, skip it + ip.Position = _space.Advance(ip.Position, ip.Delta); + ip.StackStack.Push(_space[ip.Position]); + break; + + case 's': // Store Character: store to next cell, skip it + { + var val = ip.StackStack.Pop(); + ip.Position = _space.Advance(ip.Position, ip.Delta); + _space[ip.Position] = val; + break; + } + + // ── String mode ────────────────────────────────────────────────── + case '"': // Toggle Stringmode + ip.StringMode = true; + break; + + // ── FungeSpace get/put ─────────────────────────────────────────── + case 'g': // Get: read cell at (x+offset, y+offset) + { + int y = ip.StackStack.Pop(), x = ip.StackStack.Pop(); + ip.StackStack.Push(_space[new FungeVector(x + ip.Offset.X, y + ip.Offset.Y)]); + break; + } + + case 'p': // Put: write cell at (x+offset, y+offset) + { + int y = ip.StackStack.Pop(), x = ip.StackStack.Pop(); + var val = ip.StackStack.Pop(); + _space[new FungeVector(x + ip.Offset.X, y + ip.Offset.Y)] = val; + break; + } + + // ── I/O ────────────────────────────────────────────────────────── + case '.': // Output Integer + output.Write(ip.StackStack.Pop()); + output.Write(' '); + break; + + case ',': // Output Character + output.Write((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); + break; + } + + case '~': // Input Character + { + var ch = input.Read(); + if (ch < 0) ip.Delta = ip.Delta.Reflect(); + else ip.StackStack.Push(ch); + break; + } + + // ── Control flow ───────────────────────────────────────────────── + case '@': // Stop this IP + ip.IsStopped = true; + break; + + case 'q': // Quit program immediately + exitCode = ip.StackStack.Pop(); + quit = true; + break; + + case 'k': // Iterate: execute next instruction n times + { + var n = ip.StackStack.Pop(); + + // Advance past spaces AND semicolon-delimited sections to find the operand. + // Per spec, k skips spaces and ';'-enclosed regions just as normal execution would. + var instrPos = _space.Advance(ip.Position, ip.Delta); + while (true) + { + var c = _space[instrPos]; + if (c is ' ' or '\t' or '\f' or '\v') + { + instrPos = _space.Advance(instrPos, ip.Delta); + } + else if (c == ';') + { + // skip the semicolon section entirely (same as the ';' instruction) + instrPos = _space.Advance(instrPos, ip.Delta); + while (_space[instrPos] != ';') + instrPos = _space.Advance(instrPos, ip.Delta); + instrPos = _space.Advance(instrPos, ip.Delta); + } + else + { + break; + } + } + + if (n == 0) + { + // n=0: skip the operand. IP moves to instrPos, then normal advance passes it. + ip.Position = instrPos; + } + else + { + // n>0: execute the discovered operand n times, but execute it AT k. + // The operand is only searched for; its semantics apply at the current IP position. + // 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++) + { + var dummy = false; + ExecuteInstruction(ip, ips, ipNode, ref exitCode, ref quit, ref dummy, input, output, operand); + } + } + break; + } + + // ── Concurrency ────────────────────────────────────────────────── + case 't': // Split: create child IP with reflected delta + { + var child = ip.CreateChild(_nextIpId++); + ips.AddAfter(ipNode, child); + break; + } + + // ── Stack Stack operations ──────────────────────────────────────── + case '{': // Begin Block + { + var n = ip.StackStack.Pop(); + + // Collect n items from TOSS (top item first) + var items = new List(); + if (n > 0) + for (var i = 0; i < n; i++) items.Add(ip.StackStack.Pop()); + + // Push storage offset to current TOSS (will become SOSS) + ip.StackStack.Push(ip.Offset.X); + ip.StackStack.Push(ip.Offset.Y); + + // Push new empty stack (old TOSS becomes SOSS) + ip.StackStack.PushNewStack(); + + if (n > 0) + { + // Re-push items so original top is on top of new TOSS + for (var i = items.Count - 1; i >= 0; i--) + ip.StackStack.Push(items[i]); + } + else if (n < 0) + { + // Push |n| zeros to SOSS + var soss = ip.StackStack.SOSS!; + for (var i = 0; i < -n; i++) soss.Push(0); + } + + // Set storage offset to next cell position + ip.Offset = _space.Advance(ip.Position, ip.Delta); + break; + } + + case '}': // End Block + { + var n = ip.StackStack.Pop(); + if (!ip.StackStack.HasSOSS) + { + ip.Delta = ip.Delta.Reflect(); + break; + } + + // Collect items from TOSS + var items = new List(); + for (var i = 0; i < Math.Max(0, n); i++) items.Add(ip.StackStack.Pop()); + + // Pop current TOSS (discard remaining items) + ip.StackStack.PopCurrentStack(); + + // Restore storage offset (Y on top, then X) + var oy = ip.StackStack.Pop(); + var ox = ip.StackStack.Pop(); + ip.Offset = new FungeVector(ox, oy); + + // If n < 0, discard |n| items from (now current) TOSS + if (n < 0) + for (var i = 0; i < -n; i++) ip.StackStack.Pop(); + + // Push collected items (original top on top) + for (var i = items.Count - 1; i >= 0; i--) + ip.StackStack.Push(items[i]); + break; + } + + case 'u': // Stack Under Stack + { + var n = ip.StackStack.Pop(); + if (!ip.StackStack.HasSOSS) + { + ip.Delta = ip.Delta.Reflect(); + break; + } + var soss = ip.StackStack.SOSS!; + if (n > 0) + for (var i = 0; i < n; i++) ip.StackStack.Push(soss.Count > 0 ? soss.Pop() : 0); + else if (n < 0) + for (var i = 0; i < -n; i++) soss.Push(ip.StackStack.Pop()); + break; + } + + // ── System info ────────────────────────────────────────────────── + case 'y': // Get SysInfo + { + var c = ip.StackStack.Pop(); + PushSysInfo(ip, ips.Count, c); + break; + } + + // ── Fingerprints (reflect – not implemented) ───────────────────── + case '(': // Load Semantics + { + var n = ip.StackStack.Pop(); + for (var i = 0; i < n; i++) ip.StackStack.Pop(); + ip.Delta = ip.Delta.Reflect(); + break; + } + + case ')': // Unload Semantics + { + var n = ip.StackStack.Pop(); + for (var i = 0; i < n; i++) ip.StackStack.Pop(); + ip.Delta = ip.Delta.Reflect(); + break; + } + + // ── Optional / 3-D-only (reflect) ──────────────────────────────── + case '=': // Execute (system exec) – reflect + { + // Consume 0gnirts command string from stack + while (ip.StackStack.Pop() != 0) { } + ip.Delta = ip.Delta.Reflect(); + break; + } + + case 'i': // Input File – reflect + case 'o': // Output File – reflect + case 'h': // Go High (3-D) – reflect + case 'l': // Go Low (3-D) – reflect + case 'm': // High-Low If (3-D) – reflect + ip.Delta = ip.Delta.Reflect(); + break; + + default: + // A-Z: fingerprint-defined; reflect if not loaded + if (cell is >= 'A' and <= 'Z') + ip.Delta = ip.Delta.Reflect(); + // All other characters: no-op + break; + } + } + + /// + /// Pushes system information onto the TOSS of the given IP. + /// If is greater than zero, only item + /// (1-indexed from top) is left on the stack. + /// + private 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 = []; + + // 1. Flags: bit 0 = /t (concurrency supported) + items.Add(1); + // 2. Cell size in bytes + items.Add(4); + // 3. Interpreter handprint ("Fung" as big-endian int) + items.Add(unchecked((int)0x46756E67u)); + // 4. Version (Funge-98 = 9800) + items.Add(9800); + // 5. Operating paradigm (0 = system() unavailable) + items.Add(0); + // 6. Path separator + items.Add(Path.DirectorySeparatorChar); + // 7. Number of dimensions (2 = Befunge) + items.Add(2); + // 8. IP unique ID + items.Add(ip.Id); + // 9. IP team number + items.Add(0); + // 10-11. IP position (X, Y; Y on top) + items.Add(ip.Position.X); + items.Add(ip.Position.Y); + // 12-13. IP delta (dX, dY; dY on top) + items.Add(ip.Delta.X); + items.Add(ip.Delta.Y); + // 14-15. Storage offset (oX, oY; oY on top) + items.Add(ip.Offset.X); + items.Add(ip.Offset.Y); + // 16-17. Least point of LSAB (minX, minY; minY on top) + items.Add(_space.MinX); + items.Add(_space.MinY); + // 18-19. Greatest point of LSAB (maxX, maxY; maxY on top) + items.Add(_space.MaxX); + items.Add(_space.MaxY); + + var now = DateTime.Now; + // 20. Current date: (year-1900)*10000 + month*100 + day + items.Add(((now.Year - 1900) * 10000) + (now.Month * 100) + now.Day); + // 21. Current time: HH*10000 + MM*100 + SS + items.Add((now.Hour * 10000) + (now.Minute * 100) + now.Second); + // 22. Number of stacks in stack stack + items.Add(ip.StackStack.StackCount); + // 23+. Size of each stack (TOSS first) + foreach (var stack in ip.StackStack.AllStacks) + items.Add(stack.Count); + // Command-line args: empty list (single 0 terminator) + items.Add(0); + // Environment variables: empty list (single 0 terminator) + items.Add(0); + + // Push in reverse order so items[0] ends up on top (= item 1) + for (var i = items.Count - 1; i >= 0; i--) + ip.StackStack.Push(items[i]); + + if (c > 0) + { + // Pick the c-th item from top of pushed items + var popped = new int[items.Count]; + for (var i = 0; i < items.Count; i++) + popped[i] = ip.StackStack.Pop(); + ip.StackStack.Push(c <= items.Count ? popped[c - 1] : 0); + } + } +} diff --git a/Processor/IProcessor.cs b/Processor/IProcessor.cs new file mode 100644 index 0000000..c47d91d --- /dev/null +++ b/Processor/IProcessor.cs @@ -0,0 +1,55 @@ +using System.IO.Pipelines; + +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace Esolang.Processor; +#pragma warning restore IDE0130 + +// TODO: 将来的に Esolang.Processor.Abstractions パッケージへ切り出し予定 + +/// +/// Processor の共通基底。実行対象のプログラムを保持する。 +/// +/// パース済みプログラムの型。 +public interface IProcessor +{ + /// パース済みプログラム。 + TProgram Program { get; } +} + +/// +/// / ベースの実行 IF。 +/// +/// パース済みプログラムの型。 +public interface ITextProcessor : IProcessor +{ + /// プログラムを最後まで実行し、終了コードを返す。 + int RunToEnd( + TextReader? input = null, + TextWriter? output = null, + CancellationToken cancellationToken = default); + + /// プログラムを最後まで非同期実行し、終了コードを返す。 + ValueTask RunToEndAsync( + TextReader? input = null, + TextWriter? output = null, + CancellationToken cancellationToken = default); +} + +/// +/// / ベースの実行 IF。 +/// +/// パース済みプログラムの型。 +public interface IPipeProcessor : IProcessor +{ + /// プログラムを最後まで実行し、終了コードを返す。 + int RunToEnd( + PipeReader input, + PipeWriter output, + CancellationToken cancellationToken = default); + + /// プログラムを最後まで非同期実行し、終了コードを返す。 + ValueTask RunToEndAsync( + PipeReader input, + PipeWriter output, + CancellationToken cancellationToken = default); +} From 86b34dd56cbbdc1eb3644435b2ded47ef35dac31 Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 7 May 2026 15:39:18 +0900 Subject: [PATCH 02/17] Refine InlineSource tests and adopt Assert.Contains/CancellationToken helpers --- Generator.Tests/FungeMethodGeneratorTests.cs | 1180 ++++++++++-------- 1 file changed, 638 insertions(+), 542 deletions(-) diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index da98929..3c6a58b 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -1,542 +1,638 @@ -using Basic.Reference.Assemblies; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; -using System.Collections.Immutable; -using System.Reflection; -using System.Text; - -namespace Esolang.Funge.Generator.Tests; - -[TestClass] -public class FungeMethodGeneratorTests -{ - public TestContext TestContext { get; set; } = default!; - - Compilation baseCompilation = default!; - - [TestInitialize] - public void InitializeCompilation() - { - IEnumerable references = -#if NET10_0_OR_GREATER - Net100.References.All; -#elif NET9_0_OR_GREATER - Net90.References.All; -#elif NET8_0_OR_GREATER - Net80.References.All; -#elif NET472_OR_GREATER - Net472.References.All; -#else - throw new InvalidOperationException("Unsupported target framework for generator tests."); -#endif - - var referenceList = references.ToList(); - { - var hasPipelinesReference = referenceList.Any(static r => - string.Equals(Path.GetFileNameWithoutExtension(r.FilePath), "System.IO.Pipelines", StringComparison.OrdinalIgnoreCase)); - if (!hasPipelinesReference) - { - var pipelinesAssemblyLocation = typeof(System.IO.Pipelines.PipeReader).Assembly.Location; - if (!string.IsNullOrWhiteSpace(pipelinesAssemblyLocation)) - { - referenceList.Add(MetadataReference.CreateFromFile(pipelinesAssemblyLocation)); - } - } - } -#if !NET - { - var memoryAssemblyLocation = typeof(Memory<>).Assembly.Location; - if (!string.IsNullOrWhiteSpace(memoryAssemblyLocation)) - { - referenceList.Add(MetadataReference.CreateFromFile(memoryAssemblyLocation)); - } - } - { - var asm = typeof(ValueTask).Assembly.Location; - referenceList.Add(MetadataReference.CreateFromFile(asm)); - } - { - var asm = typeof(IAsyncEnumerable<>).Assembly.Location; - referenceList.Add(MetadataReference.CreateFromFile(asm)); - } -#endif - - baseCompilation = CSharpCompilation.Create("generatortest", - references: referenceList, - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - } - - // ----------------------------------------------------------------------- - // Helpers - // ----------------------------------------------------------------------- - - GeneratorDriver RunGenerators( - string source, - out Compilation outputCompilation, - out ImmutableArray diagnostics, - IEnumerable<(string path, string content)>? additionalFiles = null, - LanguageVersion languageVersion = LanguageVersion.CSharp12, - CancellationToken cancellationToken = default) - { - var parseOptions = new CSharpParseOptions(languageVersion); - var generator = new MethodGenerator(); - var driver = CSharpGeneratorDriver.Create( - generators: [generator.AsSourceGenerator()], - additionalTexts: additionalFiles?.Select(f => - (AdditionalText)new TestAdditionalText(f.path, f.content)) ?? [], - driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true) - ).WithUpdatedParseOptions(parseOptions); - - var compilation = baseCompilation.AddSyntaxTrees( - CSharpSyntaxTree.ParseText(source, parseOptions, path: "input.cs", - encoding: Encoding.UTF8, cancellationToken: cancellationToken)); - - return driver.RunGeneratorsAndUpdateCompilation(compilation, out outputCompilation, out diagnostics, cancellationToken); - } - - Assembly Emit(Compilation compilation, CancellationToken cancellationToken = default) - { - using var ms = new MemoryStream(); - var result = compilation.Emit(ms, cancellationToken: cancellationToken); - if (!result.Success) - { - foreach (var d in result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)) - TestContext.WriteLine(d.ToString()); - foreach (var t in compilation.SyntaxTrees) - TestContext.WriteLine($"// {t.FilePath}\n{t}"); - Assert.Fail("Compilation emit failed"); - } - ms.Seek(0, SeekOrigin.Begin); - -#if NET48 - return Assembly.Load(ms.ToArray()); -#else - var ctx = new System.Runtime.Loader.AssemblyLoadContext(nameof(FungeMethodGeneratorTests), isCollectible: true); - return ctx.LoadFromStream(ms); -#endif - } - - void AssertNoErrors(ImmutableArray diagnostics, Compilation compilation) - { - var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToArray(); - if (errors.Length > 0) - { - foreach (var d in errors) TestContext.WriteLine(d.ToString()); - foreach (var t in compilation.SyntaxTrees) TestContext.WriteLine($"// {t.FilePath}\n{t}"); - Assert.Fail($"{errors.Length} error(s) in generator output"); - } - } - - // ----------------------------------------------------------------------- - // Basic tests - // ----------------------------------------------------------------------- - - [TestMethod] - public void EmptyProgram_Void_NoErrors() - { - // "@" is the Funge "stop" instruction — program terminates immediately - var source = """ - using Esolang.Funge; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial void Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "@")]); - AssertNoErrors(diag, comp); - Assert.AreEqual(4, comp.SyntaxTrees.Count()); // input.cs + attributes + helper + method - } - - [TestMethod] - public async Task HelloWorld_StringReturn() - { - // Classic Hello World in Funge-98 - const string helloWorld = - "64+\"!dlroW ,olleH\",,,,,,,,,,,,,@"; - - var source = """ - using Esolang.Funge; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("hello.b98")] - public static partial string Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("hello.b98", helloWorld)]); - AssertNoErrors(diag, comp); - - var asm = Emit(comp); - await Task.Factory.StartNew(() => - { - var t = asm.GetType("TestProject.TestClass")!; - var m = t.GetMethod("Run")!; - var result = (string?)m.Invoke(null, []); - Assert.AreEqual("Hello, World!", result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); - } - - [TestMethod] - public async Task StringMode_SgmlStyleSpaces_StringReturn() - { - const string program = "\" \"..@"; - - var source = """ - using Esolang.Funge; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("sgml.b98")] - public static partial string Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("sgml.b98", program)]); - AssertNoErrors(diag, comp); - - var asm = Emit(comp); - await Task.Factory.StartNew(() => - { - var t = asm.GetType("TestProject.TestClass")!; - var m = t.GetMethod("Run")!; - var result = (string?)m.Invoke(null, []); - Assert.AreEqual("32 0 ", result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); - } - - [TestMethod] - public async Task Iterate_K_ExecutesOperandCorrectly_StringReturn() - { - const string program = "2k6...@"; - - var source = """ - using Esolang.Funge; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("k.b98")] - public static partial string Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("k.b98", program)]); - AssertNoErrors(diag, comp); - - var asm = Emit(comp); - await Task.Factory.StartNew(() => - { - var t = asm.GetType("TestProject.TestClass")!; - var m = t.GetMethod("Run")!; - var result = (string?)m.Invoke(null, []); - Assert.AreEqual("6 6 6 ", result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); - } - - [TestMethod] - public void ReturnType_Void_TextWriter() - { - var source = """ - using Esolang.Funge; - using System.IO; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial void Run(TextWriter output); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "@")]); - AssertNoErrors(diag, comp); - } - - [TestMethod] - public void ReturnType_Task_NoErrors() - { - var source = """ - using Esolang.Funge; - using System.Threading.Tasks; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial Task Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "@")]); - AssertNoErrors(diag, comp); - } - - [TestMethod] - public void ReturnType_TaskString_NoErrors() - { - var source = """ - using Esolang.Funge; - using System.Threading.Tasks; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial Task Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "@")]); - AssertNoErrors(diag, comp); - } - - [TestMethod] - public void ReturnType_ValueTask_NoErrors() - { - var source = """ - using Esolang.Funge; - using System.Threading.Tasks; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial ValueTask Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "@")]); - AssertNoErrors(diag, comp); - } - - [TestMethod] - public void ReturnType_ValueTaskString_NoErrors() - { - var source = """ - using Esolang.Funge; - using System.Threading.Tasks; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial ValueTask Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "@")]); - AssertNoErrors(diag, comp); - } - - [TestMethod] - public void ReturnType_IEnumerableByte_NoErrors() - { - var source = """ - using Esolang.Funge; - using System.Collections.Generic; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial IEnumerable Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "@")]); - AssertNoErrors(diag, comp); - } - - [TestMethod] - public void ReturnType_IAsyncEnumerableByte_NoErrors() - { - var source = """ - using Esolang.Funge; - using System.Collections.Generic; - using System.Threading; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial IAsyncEnumerable Run(CancellationToken cancellationToken = default); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "@")]); - AssertNoErrors(diag, comp); - } - - [TestMethod] - public void Input_TextReader_NoErrors() - { - var source = """ - using Esolang.Funge; - using System.IO; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial void Run(TextReader input); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "@")]); - AssertNoErrors(diag, comp); - } - - [TestMethod] - public void Input_String_NoErrors() - { - var source = """ - using Esolang.Funge; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial void Run(string input); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "@")]); - AssertNoErrors(diag, comp); - } - - // ----------------------------------------------------------------------- - // Diagnostic tests - // ----------------------------------------------------------------------- - - [TestMethod] - public void Diagnostic_InvalidReturnType_FG0002() - { - var source = """ - using Esolang.Funge; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial int Run(); - } - """; - RunGenerators(source, out _, out var diag, - additionalFiles: [("test.b98", "@")]); - Assert.IsTrue(diag.Any(d => d.Id == "FG0002"), "Expected FG0002"); - } - - [TestMethod] - public void Diagnostic_SourceFileNotFound_FG0004() - { - var source = """ - using Esolang.Funge; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("nonexistent.b98")] - public static partial void Run(); - } - """; - RunGenerators(source, out _, out var diag); - Assert.IsTrue(diag.Any(d => d.Id == "FG0004"), "Expected FG0004"); - } - - [TestMethod] - public void Diagnostic_DuplicateInputParameter_FG0006() - { - var source = """ - using Esolang.Funge; - using System.IO; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial void Run(TextReader a, TextReader b); - } - """; - RunGenerators(source, out _, out var diag, - additionalFiles: [("test.b98", "@")]); - Assert.IsTrue(diag.Any(d => d.Id == "FG0006"), "Expected FG0006"); - } - - [TestMethod] - public void Diagnostic_ReturnOutputConflict_FG0007() - { - var source = """ - using Esolang.Funge; - using System.IO; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial string Run(TextWriter output); - } - """; - RunGenerators(source, out _, out var diag, - additionalFiles: [("test.b98", "@")]); - Assert.IsTrue(diag.Any(d => d.Id == "FG0007"), "Expected FG0007"); - } - - [TestMethod] - public void Runtime_SelfModifiedOutputWithoutOutputInterface_Throws() - { - var source = """ - using Esolang.Funge; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial void Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "68*2-s<<@")]); - AssertNoErrors(diag, comp); - - var asm = Emit(comp); - var t = asm.GetType("TestProject.TestClass") - ?? asm.GetType("TestClass"); - Assert.IsNotNull(t, "Failed to find generated type TestProject.TestClass."); - var m = t.GetMethod("Run", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance); - Assert.IsNotNull(m, "Failed to find generated method Run."); - - var ex = Assert.Throws(() => m!.Invoke(null, [])); - Assert.IsNotNull(ex); - Assert.IsNotNull(ex.InnerException); - Assert.IsInstanceOfType(ex.InnerException, typeof(InvalidOperationException)); - StringAssert.Contains(ex.InnerException!.Message, "without an output interface"); - } - - [TestMethod] - public void Runtime_SelfModifiedInputWithoutInputInterface_Throws() - { - var source = """ - using Esolang.Funge; - namespace TestProject; - partial class TestClass - { - [GenerateFungeMethod("test.b98")] - public static partial void Run(); - } - """; - RunGenerators(source, out var comp, out var diag, - additionalFiles: [("test.b98", "66*2+s<<@")]); - AssertNoErrors(diag, comp); - - var asm = Emit(comp); - var t = asm.GetType("TestProject.TestClass") - ?? asm.GetType("TestClass"); - Assert.IsNotNull(t, "Failed to find generated type TestProject.TestClass."); - var m = t.GetMethod("Run", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance); - Assert.IsNotNull(m, "Failed to find generated method Run."); - - var ex = Assert.Throws(() => m!.Invoke(null, [])); - Assert.IsNotNull(ex); - Assert.IsNotNull(ex.InnerException); - Assert.IsInstanceOfType(ex.InnerException, typeof(InvalidOperationException)); - StringAssert.Contains(ex.InnerException!.Message, "without an input interface"); - } -} - -/// Fake AdditionalText for testing. -file sealed class TestAdditionalText(string path, string content) : AdditionalText -{ - public override string Path { get; } = path; - public override SourceText? GetText(CancellationToken cancellationToken = default) - => SourceText.From(content, Encoding.UTF8); -} +using Basic.Reference.Assemblies; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using System.Collections.Immutable; +using System.Reflection; +using System.Text; + +namespace Esolang.Funge.Generator.Tests; + +[TestClass] +public class FungeMethodGeneratorTests +{ + public TestContext TestContext { get; set; } = default!; + + Compilation baseCompilation = default!; + + [TestInitialize] + public void InitializeCompilation() + { + IEnumerable references = +#if NET10_0_OR_GREATER + Net100.References.All; +#elif NET9_0_OR_GREATER + Net90.References.All; +#elif NET8_0_OR_GREATER + Net80.References.All; +#elif NET472_OR_GREATER + Net472.References.All; +#else + throw new InvalidOperationException("Unsupported target framework for generator tests."); +#endif + + var referenceList = references.ToList(); + { + var hasPipelinesReference = referenceList.Any(static r => + string.Equals(Path.GetFileNameWithoutExtension(r.FilePath), "System.IO.Pipelines", StringComparison.OrdinalIgnoreCase)); + if (!hasPipelinesReference) + { + var pipelinesAssemblyLocation = typeof(System.IO.Pipelines.PipeReader).Assembly.Location; + if (!string.IsNullOrWhiteSpace(pipelinesAssemblyLocation)) + { + referenceList.Add(MetadataReference.CreateFromFile(pipelinesAssemblyLocation)); + } + } + } +#if !NET + { + var memoryAssemblyLocation = typeof(Memory<>).Assembly.Location; + if (!string.IsNullOrWhiteSpace(memoryAssemblyLocation)) + { + referenceList.Add(MetadataReference.CreateFromFile(memoryAssemblyLocation)); + } + } + { + var asm = typeof(ValueTask).Assembly.Location; + referenceList.Add(MetadataReference.CreateFromFile(asm)); + } + { + var asm = typeof(IAsyncEnumerable<>).Assembly.Location; + referenceList.Add(MetadataReference.CreateFromFile(asm)); + } +#endif + + baseCompilation = CSharpCompilation.Create("generatortest", + references: referenceList, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + CancellationToken TestCancellationToken => TestContext.CancellationTokenSource.Token; + + GeneratorDriver RunGenerators( + string source, + out Compilation outputCompilation, + out ImmutableArray diagnostics, + IEnumerable<(string path, string content)>? additionalFiles = null, + LanguageVersion languageVersion = LanguageVersion.CSharp12) + => RunGenerators(source, out outputCompilation, out diagnostics, TestCancellationToken, additionalFiles, languageVersion); + + GeneratorDriver RunGenerators( + string source, + out Compilation outputCompilation, + out ImmutableArray diagnostics, + CancellationToken cancellationToken, + IEnumerable<(string path, string content)>? additionalFiles = null, + LanguageVersion languageVersion = LanguageVersion.CSharp12) + { + var parseOptions = new CSharpParseOptions(languageVersion); + var generator = new MethodGenerator(); + var driver = CSharpGeneratorDriver.Create( + generators: [generator.AsSourceGenerator()], + additionalTexts: additionalFiles?.Select(f => + (AdditionalText)new TestAdditionalText(f.path, f.content)) ?? [], + driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true) + ).WithUpdatedParseOptions(parseOptions); + + var compilation = baseCompilation.AddSyntaxTrees( + CSharpSyntaxTree.ParseText(source, parseOptions, path: "input.cs", + encoding: Encoding.UTF8, cancellationToken: cancellationToken)); + + return driver.RunGeneratorsAndUpdateCompilation(compilation, out outputCompilation, out diagnostics, cancellationToken); + } + + Assembly Emit(Compilation compilation) + => Emit(compilation, TestCancellationToken); + + Assembly Emit(Compilation compilation, CancellationToken cancellationToken) + { + using var ms = new MemoryStream(); + var result = compilation.Emit(ms, cancellationToken: cancellationToken); + if (!result.Success) + { + foreach (var d in result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)) + TestContext.WriteLine(d.ToString()); + foreach (var t in compilation.SyntaxTrees) + TestContext.WriteLine($"// {t.FilePath}\n{t}"); + Assert.Fail("Compilation emit failed"); + } + ms.Seek(0, SeekOrigin.Begin); + +#if NET48 + return Assembly.Load(ms.ToArray()); +#else + var ctx = new System.Runtime.Loader.AssemblyLoadContext(nameof(FungeMethodGeneratorTests), isCollectible: true); + return ctx.LoadFromStream(ms); +#endif + } + + void AssertNoErrors(ImmutableArray diagnostics, Compilation compilation) + { + var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToArray(); + if (errors.Length > 0) + { + foreach (var d in errors) TestContext.WriteLine(d.ToString()); + foreach (var t in compilation.SyntaxTrees) TestContext.WriteLine($"// {t.FilePath}\n{t}"); + Assert.Fail($"{errors.Length} error(s) in generator output"); + } + } + + // ----------------------------------------------------------------------- + // Basic tests + // ----------------------------------------------------------------------- + + [TestMethod] + public void EmptyProgram_Void_NoErrors() + { + // "@" is the Funge "stop" instruction — program terminates immediately + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial void Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + Assert.AreEqual(4, comp.SyntaxTrees.Count()); // input.cs + attributes + helper + method + } + + [TestMethod] + public async Task HelloWorld_StringReturn() + { + // Classic Hello World in Funge-98 + const string helloWorld = + "64+\"!dlroW ,olleH\",,,,,,,,,,,,,@"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("hello.b98")] + public static partial string Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("hello.b98", helloWorld)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (string?)m.Invoke(null, []); + Assert.AreEqual("Hello, World!", result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + [TestMethod] + public async Task StringMode_SgmlStyleSpaces_StringReturn() + { + const string program = "\" \"..@"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("sgml.b98")] + public static partial string Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("sgml.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (string?)m.Invoke(null, []); + Assert.AreEqual("32 0 ", result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + [TestMethod] + public async Task Iterate_K_ExecutesOperandCorrectly_StringReturn() + { + const string program = "2k6...@"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("k.b98")] + public static partial string Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("k.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (string?)m.Invoke(null, []); + Assert.AreEqual("6 6 6 ", result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + [TestMethod] + public void ReturnType_Void_TextWriter() + { + var source = """ + using Esolang.Funge; + using System.IO; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial void Run(TextWriter output); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + [TestMethod] + public void ReturnType_Task_NoErrors() + { + var source = """ + using Esolang.Funge; + using System.Threading.Tasks; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial Task Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + [TestMethod] + public void ReturnType_TaskString_NoErrors() + { + var source = """ + using Esolang.Funge; + using System.Threading.Tasks; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial Task Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + [TestMethod] + public void ReturnType_ValueTask_NoErrors() + { + var source = """ + using Esolang.Funge; + using System.Threading.Tasks; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial ValueTask Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + [TestMethod] + public void ReturnType_ValueTaskString_NoErrors() + { + var source = """ + using Esolang.Funge; + using System.Threading.Tasks; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial ValueTask Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + [TestMethod] + public void ReturnType_IEnumerableByte_NoErrors() + { + var source = """ + using Esolang.Funge; + using System.Collections.Generic; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial IEnumerable Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + [TestMethod] + public void ReturnType_IAsyncEnumerableByte_NoErrors() + { + var source = """ + using Esolang.Funge; + using System.Collections.Generic; + using System.Threading; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial IAsyncEnumerable Run(CancellationToken cancellationToken = default); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + [TestMethod] + public void Input_TextReader_NoErrors() + { + var source = """ + using Esolang.Funge; + using System.IO; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial void Run(TextReader input); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + [TestMethod] + public void Input_String_NoErrors() + { + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial void Run(string input); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + // ----------------------------------------------------------------------- + // Diagnostic tests + // ----------------------------------------------------------------------- + + [TestMethod] + public void Diagnostic_InvalidReturnType_FG0002() + { + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out _, out var diag, + additionalFiles: [("test.b98", "@")]); + Assert.IsTrue(diag.Any(d => d.Id == "FG0002"), "Expected FG0002"); + } + + [TestMethod] + public void Diagnostic_SourceFileNotFound_FG0004() + { + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("nonexistent.b98")] + public static partial void Run(); + } + """; + RunGenerators(source, out _, out var diag); + Assert.IsTrue(diag.Any(d => d.Id == "FG0004"), "Expected FG0004"); + } + + [TestMethod] + public void Diagnostic_DuplicateInputParameter_FG0006() + { + var source = """ + using Esolang.Funge; + using System.IO; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial void Run(TextReader a, TextReader b); + } + """; + RunGenerators(source, out _, out var diag, + additionalFiles: [("test.b98", "@")]); + Assert.IsTrue(diag.Any(d => d.Id == "FG0006"), "Expected FG0006"); + } + + [TestMethod] + public void Diagnostic_ReturnOutputConflict_FG0007() + { + var source = """ + using Esolang.Funge; + using System.IO; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial string Run(TextWriter output); + } + """; + RunGenerators(source, out _, out var diag, + additionalFiles: [("test.b98", "@")]); + Assert.IsTrue(diag.Any(d => d.Id == "FG0007"), "Expected FG0007"); + } + + [TestMethod] + public void Runtime_SelfModifiedOutputWithoutOutputInterface_Throws() + { + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial void Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "68*2-s<<@")]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + var t = asm.GetType("TestProject.TestClass") + ?? asm.GetType("TestClass"); + Assert.IsNotNull(t, "Failed to find generated type TestProject.TestClass."); + var m = t.GetMethod("Run", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance); + Assert.IsNotNull(m, "Failed to find generated method Run."); + + var ex = Assert.Throws(() => m!.Invoke(null, [])); + Assert.IsNotNull(ex); + Assert.IsNotNull(ex.InnerException); + Assert.IsInstanceOfType(ex.InnerException, typeof(InvalidOperationException)); + Assert.Contains("without an output interface", ex.InnerException!.Message); + } + + [TestMethod] + public void Runtime_SelfModifiedInputWithoutInputInterface_Throws() + { + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial void Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "66*2+s<<@")]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + var t = asm.GetType("TestProject.TestClass") + ?? asm.GetType("TestClass"); + Assert.IsNotNull(t, "Failed to find generated type TestProject.TestClass."); + var m = t.GetMethod("Run", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance); + Assert.IsNotNull(m, "Failed to find generated method Run."); + + var ex = Assert.Throws(() => m!.Invoke(null, [])); + Assert.IsNotNull(ex); + Assert.IsNotNull(ex.InnerException); + Assert.IsInstanceOfType(ex.InnerException, typeof(InvalidOperationException)); + Assert.Contains("without an input interface", ex.InnerException!.Message); + } + + // ----------------------------------------------------------------------- + // InlineSource with multiple lines + // ----------------------------------------------------------------------- + + [TestMethod] + public void InlineSource_RawStringWithInnerQuotes_InspectGenerated() + { + // Inspect how the generator processes raw string literals in InlineSource + // This test outputs the generated code to verify the processing + var source = """" + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod(InlineSource = """ + @ + """)] + public static partial void Run(); + } + """"; + RunGenerators(source, out var comp, out var diag); + AssertNoErrors(diag, comp); + + var generated = comp.SyntaxTrees + .Select(static t => t.ToString()) + .Single(static text => text.Contains("Generated from: ", StringComparison.Ordinal)); + Assert.Contains("__cells[(0, 0)] = 64;", generated); + + // Output all generated syntax trees for inspection + TestContext.WriteLine("=== Generated Syntax Trees ==="); + foreach (var tree in comp.SyntaxTrees) + { + TestContext.WriteLine($"\n--- {tree.FilePath} ---"); + TestContext.WriteLine(tree.GetText().ToString()); + } + } + + [TestMethod] + public void InlineSource_MultiLine_BasicProgram() + { + // Verify multiline raw string is mapped to 2D coordinates as expected. + var source = """" + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod(InlineSource = """ + >v + ^@ + """)] + public static partial void Run(); + } + """"; + RunGenerators(source, out var comp, out var diag); + AssertNoErrors(diag, comp); + + var generated = comp.SyntaxTrees + .Select(static t => t.ToString()) + .Single(static text => text.Contains("Generated from: ", StringComparison.Ordinal)); + Assert.Contains("__cells[(0, 0)] = 62;", generated); // '>' + Assert.Contains("__cells[(1, 0)] = 118;", generated); // 'v' + Assert.Contains("__cells[(0, 1)] = 94;", generated); // '^' + Assert.Contains("__cells[(1, 1)] = 64;", generated); // '@' + } + + [TestMethod] + public void InlineSource_WithEscapedNewlines() + { + // Test InlineSource with escaped newlines (\n) instead of literal raw strings + // This avoids indentation issues with raw string literals + var source = """" + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod(InlineSource = "2:.@")] + public static partial void Run(); + } + """"; + RunGenerators(source, out var comp, out var diag); + AssertNoErrors(diag, comp); + } +} + +/// Fake AdditionalText for testing. +file sealed class TestAdditionalText(string path, string content) : AdditionalText +{ + public override string Path { get; } = path; + public override SourceText? GetText(CancellationToken cancellationToken = default) + => SourceText.From(content, Encoding.UTF8); +} From ac408a1dd1aaf0c405d60e97e25ddb9d7bfd562f Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 7 May 2026 17:14:21 +0900 Subject: [PATCH 03/17] Add int/Task/ValueTask return types with q exit-code propagation --- CHANGELOG.md | 1 + Generator.Tests/FungeMethodGeneratorTests.cs | 111 +- Generator/MethodGenerator.Runtime.cs | 435 +++--- Generator/MethodGenerator.cs | 1260 +++++++++--------- Generator/README.md | 5 +- 5 files changed, 979 insertions(+), 833 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb81d4..8a12416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Processor`: `FungeProcessor` now implements `ITextProcessor` and exposes `RunToEnd(...)` / `RunToEndAsync(...)` while preserving existing `Run(...)` behavior. - `dotnet-funge` (`Esolang.Funge.Interpreter`): command execution path now calls `RunToEnd(...)`. +- `Esolang.Funge.Generator`: added return-type support for `int`, `Task`, and `ValueTask`, and aligned `q` handling to return the popped exit code (`@` returns `0`). ## [1.0.1] - 2026-05-07 diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index 3c6a58b..caff3c4 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -286,6 +286,59 @@ partial class TestClass AssertNoErrors(diag, comp); } + [TestMethod] + public void ReturnType_Int_NoErrors() + { + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + [TestMethod] + public void ReturnType_TaskInt_NoErrors() + { + var source = """ + using Esolang.Funge; + using System.Threading.Tasks; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial Task Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + + [TestMethod] + public void ReturnType_ValueTaskInt_NoErrors() + { + var source = """ + using Esolang.Funge; + using System.Threading.Tasks; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial ValueTask Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + } + [TestMethod] public void ReturnType_TaskString_NoErrors() { @@ -425,7 +478,7 @@ namespace TestProject; partial class TestClass { [GenerateFungeMethod("test.b98")] - public static partial int Run(); + public static partial double Run(); } """; RunGenerators(source, out _, out var diag, @@ -433,6 +486,62 @@ partial class TestClass Assert.IsTrue(diag.Any(d => d.Id == "FG0002"), "Expected FG0002"); } + [TestMethod] + public async Task Runtime_ExitCode_IntReturn_QReturnsStackTop() + { + const string program = "5q@"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("exit-code.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("exit-code.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (int?)m.Invoke(null, []); + Assert.AreEqual(5, result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + [TestMethod] + public async Task Runtime_ExitCode_IntReturn_AtReturnsZero() + { + const string program = "@"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("exit-code-zero.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("exit-code-zero.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (int?)m.Invoke(null, []); + Assert.AreEqual(0, result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + [TestMethod] public void Diagnostic_SourceFileNotFound_FG0004() { diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index 8155765..7611243 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -1,216 +1,219 @@ -using System.Text; - -namespace Esolang.Funge.Generator; - -partial class MethodGenerator -{ - const string FungeRuntimeFileName = "FungeRuntime.g.cs"; - - static void EmitRuntimeIfNeeded(Microsoft.CodeAnalysis.SourceProductionContext ctx, bool needed) - { - if (!needed) return; - ctx.AddSource(FungeRuntimeFileName, BuildRuntimeSource()); - } - - static string BuildRuntimeSource() => """ - // - #nullable enable - #pragma warning disable CS1591 - using System; - using System.Collections.Generic; - using System.IO; - - namespace Esolang.Funge.__Generated - { - internal static class FungeRuntime - { - internal static void Run( - Dictionary<(int, int), int> cells, - int minX, int minY, int maxX, int maxY, - TextReader input, - TextWriter output, - bool hasInput, - bool hasOutput) - { - int px = 0, py = 0, dx = 1, dy = 0; - bool stringMode = false; - var stack = new List(); - var rng = new Random(); - - int GetCell(int x, int y) => cells.TryGetValue((x, y), out var v) ? v : ' '; - void SetCell(int x, int y, int val) - { - if (val == ' ') cells.Remove((x, y)); - else cells[(x, y)] = val; - } - int Pop() { if (stack.Count == 0) return 0; var v = stack[stack.Count - 1]; stack.RemoveAt(stack.Count - 1); return v; } - void Push(int v) => stack.Add(v); - - (int nx, int ny) Advance(int x, int y, int ddx, int ddy) - { - if (maxX < minX) return (x, y); - int nx = x + ddx, ny = y + ddy; - int w = maxX - minX + 1, h = maxY - minY + 1; - if (nx < minX) nx = maxX - ((minX - nx - 1) % w); - else if (nx > maxX) nx = minX + ((nx - maxX - 1) % w); - if (ny < minY) ny = maxY - ((minY - ny - 1) % h); - else if (ny > maxY) ny = minY + ((ny - maxY - 1) % h); - return (nx, ny); - } - - bool IsSgmlSpace(int c) => c is ' ' or '\t' or '\f' or '\v'; - - bool stopped = false; - - void ExecuteInstruction(int cell, ref bool suppressAdvance) - { - switch (cell) - { - case ' ': case '\t': case '\f': case '\v': case 'z': break; - case '!': Push(Pop() == 0 ? 1 : 0); break; - case '$': Pop(); break; - case ':': { int v = Pop(); Push(v); Push(v); break; } - case '\\': { int b = Pop(), a = Pop(); Push(b); Push(a); break; } - case 'n': stack.Clear(); break; - case '+': { int b = Pop(), a = Pop(); Push(a + b); break; } - case '-': { int b = Pop(), a = Pop(); Push(a - b); break; } - case '*': { int b = Pop(), a = Pop(); Push(a * b); break; } - case '/': { int b = Pop(), a = Pop(); Push(b == 0 ? 0 : a / b); break; } - case '%': { int b = Pop(), a = Pop(); Push(b == 0 ? 0 : a % b); break; } - case '`': { int b = Pop(), a = Pop(); Push(a > b ? 1 : 0); break; } - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': Push(cell - '0'); break; - case 'a': Push(10); break; case 'b': Push(11); break; case 'c': Push(12); break; - case 'd': Push(13); break; case 'e': Push(14); break; case 'f': Push(15); break; - case '>': dx = 1; dy = 0; break; - case '<': dx = -1; dy = 0; break; - case '^': dx = 0; dy = -1; break; - case 'v': dx = 0; dy = 1; break; - case '?': - switch (rng.Next(4)) { case 0: dx=1;dy=0;break; case 1:dx=-1;dy=0;break; case 2:dx=0;dy=-1;break; default:dx=0;dy=1;break; } - break; - case '_': { int v = Pop(); dx = v == 0 ? 1 : -1; dy = 0; break; } - case '|': { int v = Pop(); dy = v == 0 ? 1 : -1; dx = 0; break; } - case '[': { int ndx = dy, ndy = -dx; dx = ndx; dy = ndy; break; } - case ']': { int ndx = -dy, ndy = dx; dx = ndx; dy = ndy; break; } - case 'r': dx = -dx; dy = -dy; break; - case 'x': { int ndy = Pop(), ndx = Pop(); dx = ndx; dy = ndy; break; } - case 'w': { int b = Pop(), a = Pop(); if(a>b){int ndx=-dy,ndy=dx;dx=ndx;dy=ndy;}else if(a= 0 ? dx : -dx, jdy = s >= 0 ? dy : -dy, abs = s < 0 ? -s : s; - for (int i = 0; i < abs; i++) (px, py) = Advance(px, py, jdx, jdy); - suppressAdvance = true; break; - } - case ';': - (px, py) = Advance(px, py, dx, dy); - while (GetCell(px, py) != ';') (px, py) = Advance(px, py, dx, dy); - break; - case '\'': (px, py) = Advance(px, py, dx, dy); Push(GetCell(px, py)); break; - case 's': { int sv = Pop(); (px, py) = Advance(px, py, dx, dy); SetCell(px, py, sv); break; } - case '"': stringMode = true; break; - case 'g': { int gy = Pop(), gx = Pop(); Push(GetCell(gx, gy)); break; } - case 'p': { int gy = Pop(), gx = Pop(), pv = Pop(); SetCell(gx, gy, pv); break; } - case '.': - if (!hasOutput) throw new InvalidOperationException("Funge output instruction '.' executed without an output interface."); - output.Write(Pop()); output.Write(' '); break; - case ',': - if (!hasOutput) throw new InvalidOperationException("Funge output instruction ',' executed without an output interface."); - output.Write((char)Pop()); break; - case '&': - { - if (!hasInput) throw new InvalidOperationException("Funge input instruction '&' executed without an input interface."); - var line = input.ReadLine(); if(line==null){dx=-dx;dy=-dy;}else Push(int.TryParse(line.Trim(),out int iv)?iv:0); break; - } - case '~': - { - if (!hasInput) throw new InvalidOperationException("Funge input instruction '~' executed without an input interface."); - int ch = input.Read(); if(ch<0){dx=-dx;dy=-dy;}else Push(ch); break; - } - case 'k': - { - int n = Pop(); - var (ix, iy) = Advance(px, py, dx, dy); - while (true) - { - int c = GetCell(ix, iy); - if (IsSgmlSpace(c)) - { - (ix, iy) = Advance(ix, iy, dx, dy); - } - else if (c == ';') - { - (ix, iy) = Advance(ix, iy, dx, dy); - while (GetCell(ix, iy) != ';') (ix, iy) = Advance(ix, iy, dx, dy); - (ix, iy) = Advance(ix, iy, dx, dy); - } - else - { - break; - } - } - - if (n == 0) - { - (px, py) = (ix, iy); - } - else - { - int operand = GetCell(ix, iy); - for (int i = 0; i < n && !stopped; i++) - { - bool dummy = false; - ExecuteInstruction(operand, ref dummy); - } - } - break; - } - case '@': stopped = true; break; - case 'q': stopped = true; break; - default: if (cell >= 'A' && cell <= 'Z') { dx = -dx; dy = -dy; } break; - } - } - - while (!stopped) - { - int cell = GetCell(px, py); - if (stringMode) - { - if (cell == '"') - { - stringMode = false; - } - else if (IsSgmlSpace(cell)) - { - Push(' '); - while (true) - { - var (nx, ny) = Advance(px, py, dx, dy); - if (IsSgmlSpace(GetCell(nx, ny))) - { - (px, py) = (nx, ny); - } - else - { - break; - } - } - } - else - { - Push(cell); - } - (px, py) = Advance(px, py, dx, dy); - continue; - } - - bool suppressAdvance = false; - ExecuteInstruction(cell, ref suppressAdvance); - if (!stopped && !suppressAdvance) (px, py) = Advance(px, py, dx, dy); - } - } - } - } - """; -} +using System.Text; + +namespace Esolang.Funge.Generator; + +partial class MethodGenerator +{ + const string FungeRuntimeFileName = "FungeRuntime.g.cs"; + + static void EmitRuntimeIfNeeded(Microsoft.CodeAnalysis.SourceProductionContext ctx, bool needed) + { + if (!needed) return; + ctx.AddSource(FungeRuntimeFileName, BuildRuntimeSource()); + } + + static string BuildRuntimeSource() => """ + // + #nullable enable + #pragma warning disable CS1591 + using System; + using System.Collections.Generic; + using System.IO; + + namespace Esolang.Funge.__Generated + { + internal static class FungeRuntime + { + internal static int Run( + Dictionary<(int, int), int> cells, + int minX, int minY, int maxX, int maxY, + TextReader input, + TextWriter output, + bool hasInput, + bool hasOutput) + { + int px = 0, py = 0, dx = 1, dy = 0; + bool stringMode = false; + var stack = new List(); + var rng = new Random(); + int exitCode = 0; + + int GetCell(int x, int y) => cells.TryGetValue((x, y), out var v) ? v : ' '; + void SetCell(int x, int y, int val) + { + if (val == ' ') cells.Remove((x, y)); + else cells[(x, y)] = val; + } + int Pop() { if (stack.Count == 0) return 0; var v = stack[stack.Count - 1]; stack.RemoveAt(stack.Count - 1); return v; } + void Push(int v) => stack.Add(v); + + (int nx, int ny) Advance(int x, int y, int ddx, int ddy) + { + if (maxX < minX) return (x, y); + int nx = x + ddx, ny = y + ddy; + int w = maxX - minX + 1, h = maxY - minY + 1; + if (nx < minX) nx = maxX - ((minX - nx - 1) % w); + else if (nx > maxX) nx = minX + ((nx - maxX - 1) % w); + if (ny < minY) ny = maxY - ((minY - ny - 1) % h); + else if (ny > maxY) ny = minY + ((ny - maxY - 1) % h); + return (nx, ny); + } + + bool IsSgmlSpace(int c) => c is ' ' or '\t' or '\f' or '\v'; + + bool stopped = false; + + void ExecuteInstruction(int cell, ref bool suppressAdvance) + { + switch (cell) + { + case ' ': case '\t': case '\f': case '\v': case 'z': break; + case '!': Push(Pop() == 0 ? 1 : 0); break; + case '$': Pop(); break; + case ':': { int v = Pop(); Push(v); Push(v); break; } + case '\\': { int b = Pop(), a = Pop(); Push(b); Push(a); break; } + case 'n': stack.Clear(); break; + case '+': { int b = Pop(), a = Pop(); Push(a + b); break; } + case '-': { int b = Pop(), a = Pop(); Push(a - b); break; } + case '*': { int b = Pop(), a = Pop(); Push(a * b); break; } + case '/': { int b = Pop(), a = Pop(); Push(b == 0 ? 0 : a / b); break; } + case '%': { int b = Pop(), a = Pop(); Push(b == 0 ? 0 : a % b); break; } + case '`': { int b = Pop(), a = Pop(); Push(a > b ? 1 : 0); break; } + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': Push(cell - '0'); break; + case 'a': Push(10); break; case 'b': Push(11); break; case 'c': Push(12); break; + case 'd': Push(13); break; case 'e': Push(14); break; case 'f': Push(15); break; + case '>': dx = 1; dy = 0; break; + case '<': dx = -1; dy = 0; break; + case '^': dx = 0; dy = -1; break; + case 'v': dx = 0; dy = 1; break; + case '?': + switch (rng.Next(4)) { case 0: dx=1;dy=0;break; case 1:dx=-1;dy=0;break; case 2:dx=0;dy=-1;break; default:dx=0;dy=1;break; } + break; + case '_': { int v = Pop(); dx = v == 0 ? 1 : -1; dy = 0; break; } + case '|': { int v = Pop(); dy = v == 0 ? 1 : -1; dx = 0; break; } + case '[': { int ndx = dy, ndy = -dx; dx = ndx; dy = ndy; break; } + case ']': { int ndx = -dy, ndy = dx; dx = ndx; dy = ndy; break; } + case 'r': dx = -dx; dy = -dy; break; + case 'x': { int ndy = Pop(), ndx = Pop(); dx = ndx; dy = ndy; break; } + case 'w': { int b = Pop(), a = Pop(); if(a>b){int ndx=-dy,ndy=dx;dx=ndx;dy=ndy;}else if(a= 0 ? dx : -dx, jdy = s >= 0 ? dy : -dy, abs = s < 0 ? -s : s; + for (int i = 0; i < abs; i++) (px, py) = Advance(px, py, jdx, jdy); + suppressAdvance = true; break; + } + case ';': + (px, py) = Advance(px, py, dx, dy); + while (GetCell(px, py) != ';') (px, py) = Advance(px, py, dx, dy); + break; + case '\'': (px, py) = Advance(px, py, dx, dy); Push(GetCell(px, py)); break; + case 's': { int sv = Pop(); (px, py) = Advance(px, py, dx, dy); SetCell(px, py, sv); break; } + case '"': stringMode = true; break; + case 'g': { int gy = Pop(), gx = Pop(); Push(GetCell(gx, gy)); break; } + case 'p': { int gy = Pop(), gx = Pop(), pv = Pop(); SetCell(gx, gy, pv); break; } + case '.': + if (!hasOutput) throw new InvalidOperationException("Funge output instruction '.' executed without an output interface."); + output.Write(Pop()); output.Write(' '); break; + case ',': + if (!hasOutput) throw new InvalidOperationException("Funge output instruction ',' executed without an output interface."); + output.Write((char)Pop()); break; + case '&': + { + if (!hasInput) throw new InvalidOperationException("Funge input instruction '&' executed without an input interface."); + var line = input.ReadLine(); if(line==null){dx=-dx;dy=-dy;}else Push(int.TryParse(line.Trim(),out int iv)?iv:0); break; + } + case '~': + { + if (!hasInput) throw new InvalidOperationException("Funge input instruction '~' executed without an input interface."); + int ch = input.Read(); if(ch<0){dx=-dx;dy=-dy;}else Push(ch); break; + } + case 'k': + { + int n = Pop(); + var (ix, iy) = Advance(px, py, dx, dy); + while (true) + { + int c = GetCell(ix, iy); + if (IsSgmlSpace(c)) + { + (ix, iy) = Advance(ix, iy, dx, dy); + } + else if (c == ';') + { + (ix, iy) = Advance(ix, iy, dx, dy); + while (GetCell(ix, iy) != ';') (ix, iy) = Advance(ix, iy, dx, dy); + (ix, iy) = Advance(ix, iy, dx, dy); + } + else + { + break; + } + } + + if (n == 0) + { + (px, py) = (ix, iy); + } + else + { + int operand = GetCell(ix, iy); + for (int i = 0; i < n && !stopped; i++) + { + bool dummy = false; + ExecuteInstruction(operand, ref dummy); + } + } + break; + } + case '@': stopped = true; break; + case 'q': exitCode = Pop(); stopped = true; break; + default: if (cell >= 'A' && cell <= 'Z') { dx = -dx; dy = -dy; } break; + } + } + + while (!stopped) + { + int cell = GetCell(px, py); + if (stringMode) + { + if (cell == '"') + { + stringMode = false; + } + else if (IsSgmlSpace(cell)) + { + Push(' '); + while (true) + { + var (nx, ny) = Advance(px, py, dx, dy); + if (IsSgmlSpace(GetCell(nx, ny))) + { + (px, py) = (nx, ny); + } + else + { + break; + } + } + } + else + { + Push(cell); + } + (px, py) = Advance(px, py, dx, dy); + continue; + } + + bool suppressAdvance = false; + ExecuteInstruction(cell, ref suppressAdvance); + if (!stopped && !suppressAdvance) (px, py) = Advance(px, py, dx, dy); + } + + return exitCode; + } + } + } + """; +} diff --git a/Generator/MethodGenerator.cs b/Generator/MethodGenerator.cs index 27e82d9..b84149d 100644 --- a/Generator/MethodGenerator.cs +++ b/Generator/MethodGenerator.cs @@ -1,615 +1,645 @@ -using Esolang.Funge.Parser; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Collections.Immutable; -using System.Text; - -namespace Esolang.Funge.Generator; - -/// -/// A source generator that emits method implementations from GenerateFungeMethodAttribute. -/// -[Generator(LanguageNames.CSharp)] -public sealed partial class MethodGenerator : IIncrementalGenerator -{ - /// The standard auto-generated file header. - public const string CommentAutoGenerated = "// "; - - /// The namespace where the generated attribute is placed. - public const string NameSpaceName = "Esolang.Funge"; - - /// The generated attribute class name. - public const string AttributeName = "GenerateFungeMethodAttribute"; - - /// The source file name used to aggregate generated methods. - public const string GeneratedMethodsFileName = "GenerateFungeMethod.g.cs"; - - const string GeneratedMethodsFileHeader = $$""" - {{CommentAutoGenerated}} - #nullable enable - #pragma warning disable CS0219 - #pragma warning disable CS1998 - - """; - - // ----------------------------------------------------------------------- - // Enumerations for execution signature binding - // ----------------------------------------------------------------------- - - enum ReturnKind - { - Void, - String, - Task, - TaskString, - ValueTask, - ValueTaskString, - EnumerableByte, - AsyncEnumerableByte, - Invalid, - } - - enum InputKind { None, String, TextReader, PipeReader } - - enum OutputKind { None, TextWriter, PipeWriter, ReturnString, ReturnEnumerable, ReturnAsyncEnumerable } - - readonly struct ExecutionBinding( - bool isValid, - ReturnKind returnKind, - InputKind inputKind, - OutputKind outputKind, - string inputExpression, - string outputExpression, - string? cancellationTokenName, - 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? CancellationTokenName { get; } = cancellationTokenName; - 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; - } - - // ----------------------------------------------------------------------- - // IIncrementalGenerator.Initialize - // ----------------------------------------------------------------------- - - /// - public void Initialize(IncrementalGeneratorInitializationContext context) - { - context.RegisterPostInitializationOutput(static ctx => - ctx.AddSource("GenerateFungeMethodAttribute.cs", $$""" - {{CommentAutoGenerated}} - using System; - using System.Diagnostics; - namespace {{NameSpaceName}} { - [Conditional("COMPILE_TIME_ONLY")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - internal sealed class {{AttributeName}} : Attribute - { - /// - /// Generate a Funge-98 method implementation from a source file. - /// - /// Path to the Funge-98 source file (.b98). Ignored when is set. - internal {{AttributeName}}(string sourcePath = "") { } - /// - /// Inline Funge-98 source code. When non-empty, sourcePath is ignored. - /// - public string InlineSource = ""; - } - } - """)); - - const string fullAttributeName = NameSpaceName + "." + AttributeName; - var source = context.SyntaxProvider.ForAttributeWithMetadataName( - fullAttributeName, - static (node, _) => node is MethodDeclarationSyntax, - static (ctx, _) => ctx); - - var generatedTargets = source.Collect(); - - var additionalFiles = context.AdditionalTextsProvider - .Select(static (text, token) => (text.Path, Text: text.GetText(token)?.ToString())) - .Collect(); - - var languageVersion = context.ParseOptionsProvider - .Select(static (opts, _) => - opts is CSharpParseOptions csOpts ? csOpts.LanguageVersion : LanguageVersion.Default); - - var projectDirectory = context.AnalyzerConfigOptionsProvider - .Select(static (provider, _) => - provider.GlobalOptions.TryGetValue("build_property.MSBuildProjectDirectory", out var dir) - || provider.GlobalOptions.TryGetValue("build_property.ProjectDir", out dir) - ? dir : null); - - var inputs = generatedTargets - .Combine(additionalFiles) - .Combine(languageVersion) - .Combine(projectDirectory); - - context.RegisterSourceOutput(inputs, static (ctx, input) => - { - var (((sources, files), langVersion), projDir) = input; - - if (sources.IsDefaultOrEmpty) - return; - - var methodSb = new StringBuilder(GeneratedMethodsFileHeader); - var emittedCount = 0; - - foreach (var syntaxCtx in sources) - { - var method = (MethodDeclarationSyntax)syntaxCtx.TargetNode; - var symbol = (IMethodSymbol)syntaxCtx.TargetSymbol; - - if (!IsLanguageVersionAtLeastCSharp8(langVersion)) - { - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.LanguageVersionTooLow, - method.Identifier.GetLocation(), - langVersion.ToDisplayString())); - } - - // Get sourcePath attribute argument - var attrData = syntaxCtx.Attributes.FirstOrDefault(); - if (attrData is null) continue; - - var sourcePath = attrData.ConstructorArguments.FirstOrDefault().Value as string; - - // Check for inline source (named argument takes priority over file path) - string? inlineSource = null; - foreach (var namedArg in attrData.NamedArguments) - { - if (namedArg.Key == "InlineSource") - { - inlineSource = namedArg.Value.Value as string; - break; - } - } - - if (string.IsNullOrWhiteSpace(inlineSource) && string.IsNullOrWhiteSpace(sourcePath)) - { - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.InvalidSourcePathParameter, - method.Identifier.GetLocation(), - symbol.Name)); - continue; - } - - // Bind the method signature - var binding = BindExecutionSignature(symbol, method); - 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)); - continue; - } - - // Resolve source text: inline takes priority over file - string? sourceText = null; - if (!string.IsNullOrWhiteSpace(inlineSource)) - { - sourceText = inlineSource; - } - else - { - foreach (var (filePath, fileText) in files) - { - if (filePath is null || fileText is null) continue; - var normalizedSource = NormalizePath(sourcePath!); - var normalizedFile = NormalizePath(filePath); - // 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; - if (string.Equals(compareFile, normalizedSource, StringComparison.OrdinalIgnoreCase) - || compareFile.EndsWith("/" + normalizedSource, StringComparison.OrdinalIgnoreCase) - || string.Equals(System.IO.Path.GetFileName(compareFile), - System.IO.Path.GetFileName(normalizedSource), StringComparison.OrdinalIgnoreCase)) - { - sourceText = fileText; - break; - } - } - - if (sourceText is null) - { - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.SourceFileNotFound, - method.Identifier.GetLocation(), - sourcePath)); - continue; - } - } - - // Parse the Funge space - var space = FungeParser.Parse(sourceText!); - - // Scan for I/O usage - var (usesOutput, usesInput) = ScanFungeIo(space); - - if (usesOutput && !binding.HasExplicitOutput) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.RequiredOutputInterface, - method.Identifier.GetLocation(), symbol.Name)); - - if (usesInput && !binding.HasExplicitInput) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.RequiredInputInterface, - method.Identifier.GetLocation(), symbol.Name)); - - if (!usesInput && binding.HasExplicitInput) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UnusedInputInterface, - method.Identifier.GetLocation(), symbol.Name)); - - var displayPath = !string.IsNullOrWhiteSpace(inlineSource) ? "" : sourcePath!; - var emitted = EmitMethod(symbol, method, space, binding, projDir, displayPath); - methodSb.AppendLine(emitted); - emittedCount++; - } - - if (emittedCount > 0) - ctx.AddSource(GeneratedMethodsFileName, methodSb.ToString()); - - EmitRuntimeIfNeeded(ctx, emittedCount > 0); - }); - } - - // ----------------------------------------------------------------------- - // Signature binding - // ----------------------------------------------------------------------- - - static ExecutionBinding BindExecutionSignature(IMethodSymbol method, MethodDeclarationSyntax syntax) - { - var returnKind = method.ReturnType switch - { - { SpecialType: SpecialType.System_Void } => ReturnKind.Void, - { Name: "String", ContainingNamespace.Name: "System" } => ReturnKind.String, - INamedTypeSymbol t when t.Name == "Task" && t.TypeArguments.Length == 0 - => ReturnKind.Task, - INamedTypeSymbol t when t.Name == "Task" && t.TypeArguments.Length == 1 - && t.TypeArguments[0].SpecialType == SpecialType.System_String - => ReturnKind.TaskString, - INamedTypeSymbol t when t.Name == "ValueTask" && t.TypeArguments.Length == 0 - => ReturnKind.ValueTask, - INamedTypeSymbol t when t.Name == "ValueTask" && t.TypeArguments.Length == 1 - && t.TypeArguments[0].SpecialType == SpecialType.System_String - => ReturnKind.ValueTaskString, - INamedTypeSymbol t when t.Name == "IEnumerable" && t.TypeArguments.Length == 1 - && t.TypeArguments[0].SpecialType == SpecialType.System_Byte - => ReturnKind.EnumerableByte, - INamedTypeSymbol t when t.Name == "IAsyncEnumerable" && t.TypeArguments.Length == 1 - && t.TypeArguments[0].SpecialType == SpecialType.System_Byte - => ReturnKind.AsyncEnumerableByte, - _ => ReturnKind.Invalid, - }; - - if (returnKind == ReturnKind.Invalid) - return new(false, returnKind, InputKind.None, OutputKind.None, "", "", null, - 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? cancellationTokenName = null; - var hasCancellationToken = false; - - foreach (var p in method.Parameters) - { - if (p.RefKind is not RefKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, - DiagnosticDescriptors.InvalidParameter.Id, p.Locations.FirstOrDefault()); - - if (p.Type.SpecialType == SpecialType.System_String) - { - if (inputKind is not InputKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); - inputKind = InputKind.String; - inputExpr = p.Name; - continue; - } - - var typeName = p.Type.ToDisplayString(); - - if (typeName == "System.IO.TextReader") - { - if (inputKind is not InputKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); - inputKind = InputKind.TextReader; - inputExpr = p.Name; - continue; - } - - if (typeName == "System.IO.Pipelines.PipeReader") - { - if (inputKind is not InputKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); - inputKind = InputKind.PipeReader; - inputExpr = p.Name; - continue; - } - - if (typeName == "System.IO.TextWriter") - { - if (returnKind is not ReturnKind.Void) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, - cancellationTokenName, DiagnosticDescriptors.ReturnOutputConflict.Id, - p.Locations.FirstOrDefault()); - if (outputKind is not OutputKind.None) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, - cancellationTokenName, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - outputKind = OutputKind.TextWriter; - outputExpr = p.Name; - continue; - } - - if (typeName == "System.IO.Pipelines.PipeWriter") - { - if (returnKind is not ReturnKind.Void) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, - cancellationTokenName, DiagnosticDescriptors.ReturnOutputConflict.Id, - p.Locations.FirstOrDefault()); - if (outputKind is not OutputKind.None) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, - cancellationTokenName, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - outputKind = OutputKind.PipeWriter; - outputExpr = p.Name; - continue; - } - - if (typeName == "System.Threading.CancellationToken") - { - if (hasCancellationToken) - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, - cancellationTokenName, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - hasCancellationToken = true; - cancellationTokenName = p.Name; - continue; - } - - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, - cancellationTokenName, DiagnosticDescriptors.InvalidParameter.Id, - p.Locations.FirstOrDefault()); - } - - return new(true, returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, null); - } - - // ----------------------------------------------------------------------- - // Method emit - // ----------------------------------------------------------------------- - - static string EmitMethod( - IMethodSymbol symbol, - MethodDeclarationSyntax syntax, - FungeSpace space, - ExecutionBinding binding, - string? projDir, - string sourcePath) - { - var ns = symbol.ContainingType.ContainingNamespace?.IsGlobalNamespace == false - ? symbol.ContainingType.ContainingNamespace.ToDisplayString() : null; - - var typeKeyword = symbol.ContainingType.TypeKind switch - { - TypeKind.Struct when symbol.ContainingType.IsRecord => "record struct", - TypeKind.Struct => "struct", - TypeKind.Interface => "interface", - TypeKind.Class when symbol.ContainingType.IsRecord => "record", - _ => "class", - }; - - 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 returnTypeSyntax = symbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - var paramList = string.Join(", ", System.Linq.Enumerable.Select(symbol.Parameters, p => - { - var prefix = (binding.ReturnKind == ReturnKind.AsyncEnumerableByte - && binding.CancellationTokenName is not null - && string.Equals(p.Name, binding.CancellationTokenName, StringComparison.Ordinal)) - ? "[global::System.Runtime.CompilerServices.EnumeratorCancellation] " - : string.Empty; - return prefix + p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + " " + p.Name; - })); - - var relPath = projDir is not null ? MakeRelative(projDir, sourcePath) : sourcePath; - var sb = new StringBuilder(); - - if (ns is not null) { sb.Append("namespace ").Append(ns).AppendLine(" {").AppendLine(); } - sb.Append("partial ").Append(typeKeyword).Append(' ').AppendLine(typeName); - sb.AppendLine("{"); - sb.AppendLine($" // Generated from: {relPath}"); - sb.Append(" ").Append(accessibility).Append(staticMod).Append(asyncMod) - .Append(" partial ").Append(returnTypeSyntax).Append(' ') - .Append(symbol.Name).Append('(').Append(paramList).AppendLine(")"); - sb.AppendLine(" {"); - - EmitBody(sb, space, binding); - - sb.AppendLine(" }"); - sb.AppendLine("}"); - if (ns is not null) sb.AppendLine("}"); - return sb.ToString(); - } - - static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding 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())", - _ => "global::System.IO.TextReader.Null", - }; - - EmitSpaceData(sb, space); - - switch (binding.ReturnKind) - { - case ReturnKind.String: - case ReturnKind.TaskString: - case ReturnKind.ValueTaskString: - case ReturnKind.EnumerableByte: - case ReturnKind.AsyncEnumerableByte: - sb.AppendLine(" var __fungeOutput = new global::System.IO.StringWriter();"); - EmitRuntimeRunCall(sb, inputExpr, "__fungeOutput", binding.HasExplicitInput, binding.HasExplicitOutput); - break; - case ReturnKind.Void: - case ReturnKind.Task: - case ReturnKind.ValueTask: - { - if (binding.OutputKind == OutputKind.PipeWriter) - { - sb.AppendLine($" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true);"); - EmitRuntimeRunCall(sb, inputExpr, "__fungeOutput", binding.HasExplicitInput, binding.HasExplicitOutput); - } - else - { - var outExpr = binding.OutputKind == OutputKind.TextWriter - ? binding.OutputExpression - : "global::System.IO.TextWriter.Null"; - EmitRuntimeRunCall(sb, inputExpr, outExpr, binding.HasExplicitInput, binding.HasExplicitOutput); - } - break; - } - } - - switch (binding.ReturnKind) - { - case ReturnKind.String: - sb.AppendLine(" return __fungeOutput.ToString();"); - break; - case ReturnKind.Task: - sb.AppendLine(" return global::System.Threading.Tasks.Task.CompletedTask;"); - break; - case ReturnKind.TaskString: - sb.AppendLine(" return global::System.Threading.Tasks.Task.FromResult(__fungeOutput.ToString());"); - break; - case ReturnKind.ValueTask: - sb.AppendLine(" return default(global::System.Threading.Tasks.ValueTask);"); - break; - case ReturnKind.ValueTaskString: - sb.AppendLine(" return new global::System.Threading.Tasks.ValueTask(__fungeOutput.ToString());"); - break; - case ReturnKind.EnumerableByte: - case ReturnKind.AsyncEnumerableByte: - sb.AppendLine(" foreach (var __b in global::System.Text.Encoding.UTF8.GetBytes(__fungeOutput.ToString()))"); - sb.AppendLine(" yield return __b;"); - break; - } - } - - static void EmitRuntimeRunCall(StringBuilder sb, string inputExpr, string outputExpr, bool hasInput, bool hasOutput) - { - sb.AppendLine(" global::Esolang.Funge.__Generated.FungeRuntime.Run("); - sb.AppendLine($" __cells, __minX, __minY, __maxX, __maxY, {inputExpr}, {outputExpr}, {(hasInput ? "true" : "false")}, {(hasOutput ? "true" : "false")});"); - } - - static void EmitSpaceData(StringBuilder sb, FungeSpace space) - { - sb.AppendLine($" int __minX = {space.MinX}, __minY = {space.MinY}, __maxX = {space.MaxX}, __maxY = {space.MaxY};"); - sb.AppendLine(" var __cells = new global::System.Collections.Generic.Dictionary<(int, int), int>();"); - for (var y = space.MinY; y <= space.MaxY; y++) - for (var x = space.MinX; x <= space.MaxX; x++) - { - var val = space[new FungeVector(x, y)]; - if (val != ' ') - sb.AppendLine($" __cells[({x}, {y})] = {val};"); - } - } - - // ----------------------------------------------------------------------- - // I/O scan - // ----------------------------------------------------------------------- - - static (bool usesOutput, bool usesInput) ScanFungeIo(FungeSpace space) - { - bool usesOutput = false, usesInput = false; - for (var y = space.MinY; y <= space.MaxY; y++) - for (var x = space.MinX; x <= space.MaxX; x++) - { - var c = space[new FungeVector(x, y)]; - if (c is '.' or ',') usesOutput = true; - if (c is '&' or '~') usesInput = true; - if (usesOutput && usesInput) return (true, true); - } - return (usesOutput, usesInput); - } - - // ----------------------------------------------------------------------- - // Helpers - // ----------------------------------------------------------------------- - - static string GetAccessibility(Accessibility accessibility) => accessibility switch - { - Accessibility.Public => "public", - Accessibility.Protected => "protected", - Accessibility.Internal => "internal", - Accessibility.Private => "private", - Accessibility.ProtectedAndInternal => "private protected", - Accessibility.ProtectedOrInternal => "protected internal", - _ => string.Empty, - }; - - static bool IsLanguageVersionAtLeastCSharp8(LanguageVersion v) => v switch - { - LanguageVersion.Default => true, - LanguageVersion.Latest => true, - LanguageVersion.Preview => true, - LanguageVersion.LatestMajor => true, - _ => v >= LanguageVersion.CSharp8, - }; - - static string NormalizePath(string path) => path.Replace('\\', '/').TrimStart('/'); - - 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; - } -} +using Esolang.Funge.Parser; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Text; + +namespace Esolang.Funge.Generator; + +/// +/// A source generator that emits method implementations from GenerateFungeMethodAttribute. +/// +[Generator(LanguageNames.CSharp)] +public sealed partial class MethodGenerator : IIncrementalGenerator +{ + /// The standard auto-generated file header. + public const string CommentAutoGenerated = "// "; + + /// The namespace where the generated attribute is placed. + public const string NameSpaceName = "Esolang.Funge"; + + /// The generated attribute class name. + public const string AttributeName = "GenerateFungeMethodAttribute"; + + /// The source file name used to aggregate generated methods. + public const string GeneratedMethodsFileName = "GenerateFungeMethod.g.cs"; + + const string GeneratedMethodsFileHeader = $$""" + {{CommentAutoGenerated}} + #nullable enable + #pragma warning disable CS0219 + #pragma warning disable CS1998 + + """; + + // ----------------------------------------------------------------------- + // Enumerations for execution signature binding + // ----------------------------------------------------------------------- + + enum ReturnKind + { + 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 ExecutionBinding( + bool isValid, + ReturnKind returnKind, + InputKind inputKind, + OutputKind outputKind, + string inputExpression, + string outputExpression, + string? cancellationTokenName, + 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? CancellationTokenName { get; } = cancellationTokenName; + 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; + } + + // ----------------------------------------------------------------------- + // IIncrementalGenerator.Initialize + // ----------------------------------------------------------------------- + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(static ctx => + ctx.AddSource("GenerateFungeMethodAttribute.cs", $$""" + {{CommentAutoGenerated}} + using System; + using System.Diagnostics; + namespace {{NameSpaceName}} { + [Conditional("COMPILE_TIME_ONLY")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + internal sealed class {{AttributeName}} : Attribute + { + /// + /// Generate a Funge-98 method implementation from a source file. + /// + /// Path to the Funge-98 source file (.b98). Ignored when is set. + internal {{AttributeName}}(string sourcePath = "") { } + /// + /// Inline Funge-98 source code. When non-empty, sourcePath is ignored. + /// + public string InlineSource = ""; + } + } + """)); + + const string fullAttributeName = NameSpaceName + "." + AttributeName; + var source = context.SyntaxProvider.ForAttributeWithMetadataName( + fullAttributeName, + static (node, _) => node is MethodDeclarationSyntax, + static (ctx, _) => ctx); + + var generatedTargets = source.Collect(); + + var additionalFiles = context.AdditionalTextsProvider + .Select(static (text, token) => (text.Path, Text: text.GetText(token)?.ToString())) + .Collect(); + + var languageVersion = context.ParseOptionsProvider + .Select(static (opts, _) => + opts is CSharpParseOptions csOpts ? csOpts.LanguageVersion : LanguageVersion.Default); + + var projectDirectory = context.AnalyzerConfigOptionsProvider + .Select(static (provider, _) => + provider.GlobalOptions.TryGetValue("build_property.MSBuildProjectDirectory", out var dir) + || provider.GlobalOptions.TryGetValue("build_property.ProjectDir", out dir) + ? dir : null); + + var inputs = generatedTargets + .Combine(additionalFiles) + .Combine(languageVersion) + .Combine(projectDirectory); + + context.RegisterSourceOutput(inputs, static (ctx, input) => + { + var (((sources, files), langVersion), projDir) = input; + + if (sources.IsDefaultOrEmpty) + return; + + var methodSb = new StringBuilder(GeneratedMethodsFileHeader); + var emittedCount = 0; + + foreach (var syntaxCtx in sources) + { + var method = (MethodDeclarationSyntax)syntaxCtx.TargetNode; + var symbol = (IMethodSymbol)syntaxCtx.TargetSymbol; + + if (!IsLanguageVersionAtLeastCSharp8(langVersion)) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.LanguageVersionTooLow, + method.Identifier.GetLocation(), + langVersion.ToDisplayString())); + } + + // Get sourcePath attribute argument + var attrData = syntaxCtx.Attributes.FirstOrDefault(); + if (attrData is null) continue; + + var sourcePath = attrData.ConstructorArguments.FirstOrDefault().Value as string; + + // Check for inline source (named argument takes priority over file path) + string? inlineSource = null; + foreach (var namedArg in attrData.NamedArguments) + { + if (namedArg.Key == "InlineSource") + { + inlineSource = namedArg.Value.Value as string; + break; + } + } + + if (string.IsNullOrWhiteSpace(inlineSource) && string.IsNullOrWhiteSpace(sourcePath)) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.InvalidSourcePathParameter, + method.Identifier.GetLocation(), + symbol.Name)); + continue; + } + + // Bind the method signature + var binding = BindExecutionSignature(symbol, method); + 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)); + continue; + } + + // Resolve source text: inline takes priority over file + string? sourceText = null; + if (!string.IsNullOrWhiteSpace(inlineSource)) + { + sourceText = inlineSource; + } + else + { + foreach (var (filePath, fileText) in files) + { + if (filePath is null || fileText is null) continue; + var normalizedSource = NormalizePath(sourcePath!); + var normalizedFile = NormalizePath(filePath); + // 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; + if (string.Equals(compareFile, normalizedSource, StringComparison.OrdinalIgnoreCase) + || compareFile.EndsWith("/" + normalizedSource, StringComparison.OrdinalIgnoreCase) + || string.Equals(System.IO.Path.GetFileName(compareFile), + System.IO.Path.GetFileName(normalizedSource), StringComparison.OrdinalIgnoreCase)) + { + sourceText = fileText; + break; + } + } + + if (sourceText is null) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.SourceFileNotFound, + method.Identifier.GetLocation(), + sourcePath)); + continue; + } + } + + // Parse the Funge space + var space = FungeParser.Parse(sourceText!); + + // Scan for I/O usage + var (usesOutput, usesInput) = ScanFungeIo(space); + + if (usesOutput && !binding.HasExplicitOutput) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.RequiredOutputInterface, + method.Identifier.GetLocation(), symbol.Name)); + + if (usesInput && !binding.HasExplicitInput) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.RequiredInputInterface, + method.Identifier.GetLocation(), symbol.Name)); + + if (!usesInput && binding.HasExplicitInput) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UnusedInputInterface, + method.Identifier.GetLocation(), symbol.Name)); + + var displayPath = !string.IsNullOrWhiteSpace(inlineSource) ? "" : sourcePath!; + var emitted = EmitMethod(symbol, method, space, binding, projDir, displayPath); + methodSb.AppendLine(emitted); + emittedCount++; + } + + if (emittedCount > 0) + ctx.AddSource(GeneratedMethodsFileName, methodSb.ToString()); + + EmitRuntimeIfNeeded(ctx, emittedCount > 0); + }); + } + + // ----------------------------------------------------------------------- + // Signature binding + // ----------------------------------------------------------------------- + + static ExecutionBinding BindExecutionSignature(IMethodSymbol method, MethodDeclarationSyntax syntax) + { + var returnKind = method.ReturnType switch + { + { SpecialType: SpecialType.System_Void } => ReturnKind.Void, + { SpecialType: SpecialType.System_Int32 } => ReturnKind.Int, + { Name: "String", ContainingNamespace.Name: "System" } => ReturnKind.String, + INamedTypeSymbol t when t.Name == "Task" && t.TypeArguments.Length == 0 + => ReturnKind.Task, + INamedTypeSymbol t when t.Name == "Task" && t.TypeArguments.Length == 1 + && t.TypeArguments[0].SpecialType == SpecialType.System_Int32 + => ReturnKind.TaskInt, + INamedTypeSymbol t when t.Name == "Task" && t.TypeArguments.Length == 1 + && t.TypeArguments[0].SpecialType == SpecialType.System_String + => ReturnKind.TaskString, + INamedTypeSymbol t when t.Name == "ValueTask" && t.TypeArguments.Length == 0 + => ReturnKind.ValueTask, + INamedTypeSymbol t when t.Name == "ValueTask" && t.TypeArguments.Length == 1 + && t.TypeArguments[0].SpecialType == SpecialType.System_Int32 + => ReturnKind.ValueTaskInt, + INamedTypeSymbol t when t.Name == "ValueTask" && t.TypeArguments.Length == 1 + && t.TypeArguments[0].SpecialType == SpecialType.System_String + => ReturnKind.ValueTaskString, + INamedTypeSymbol t when t.Name == "IEnumerable" && t.TypeArguments.Length == 1 + && t.TypeArguments[0].SpecialType == SpecialType.System_Byte + => ReturnKind.EnumerableByte, + INamedTypeSymbol t when t.Name == "IAsyncEnumerable" && t.TypeArguments.Length == 1 + && t.TypeArguments[0].SpecialType == SpecialType.System_Byte + => ReturnKind.AsyncEnumerableByte, + _ => ReturnKind.Invalid, + }; + + if (returnKind == ReturnKind.Invalid) + return new(false, returnKind, InputKind.None, OutputKind.None, "", "", null, + 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? cancellationTokenName = null; + var hasCancellationToken = false; + + foreach (var p in method.Parameters) + { + if (p.RefKind is not RefKind.None) + return new(false, returnKind, inputKind, outputKind, "", "", null, + DiagnosticDescriptors.InvalidParameter.Id, p.Locations.FirstOrDefault()); + + if (p.Type.SpecialType == SpecialType.System_String) + { + if (inputKind is not InputKind.None) + return new(false, returnKind, inputKind, outputKind, "", "", null, + DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); + inputKind = InputKind.String; + inputExpr = p.Name; + continue; + } + + var typeName = p.Type.ToDisplayString(); + + if (typeName == "System.IO.TextReader") + { + if (inputKind is not InputKind.None) + return new(false, returnKind, inputKind, outputKind, "", "", null, + DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); + inputKind = InputKind.TextReader; + inputExpr = p.Name; + continue; + } + + if (typeName == "System.IO.Pipelines.PipeReader") + { + if (inputKind is not InputKind.None) + return new(false, returnKind, inputKind, outputKind, "", "", null, + DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); + inputKind = InputKind.PipeReader; + inputExpr = p.Name; + continue; + } + + if (typeName == "System.IO.TextWriter") + { + if (returnKind is not ReturnKind.Void) + return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, + cancellationTokenName, DiagnosticDescriptors.ReturnOutputConflict.Id, + p.Locations.FirstOrDefault()); + if (outputKind is not OutputKind.None) + return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, + cancellationTokenName, DiagnosticDescriptors.DuplicateParameter.Id, + p.Locations.FirstOrDefault()); + outputKind = OutputKind.TextWriter; + outputExpr = p.Name; + continue; + } + + if (typeName == "System.IO.Pipelines.PipeWriter") + { + if (returnKind is not ReturnKind.Void) + return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, + cancellationTokenName, DiagnosticDescriptors.ReturnOutputConflict.Id, + p.Locations.FirstOrDefault()); + if (outputKind is not OutputKind.None) + return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, + cancellationTokenName, DiagnosticDescriptors.DuplicateParameter.Id, + p.Locations.FirstOrDefault()); + outputKind = OutputKind.PipeWriter; + outputExpr = p.Name; + continue; + } + + if (typeName == "System.Threading.CancellationToken") + { + if (hasCancellationToken) + return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, + cancellationTokenName, DiagnosticDescriptors.DuplicateParameter.Id, + p.Locations.FirstOrDefault()); + hasCancellationToken = true; + cancellationTokenName = p.Name; + continue; + } + + return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, + cancellationTokenName, DiagnosticDescriptors.InvalidParameter.Id, + p.Locations.FirstOrDefault()); + } + + return new(true, returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, null); + } + + // ----------------------------------------------------------------------- + // Method emit + // ----------------------------------------------------------------------- + + static string EmitMethod( + IMethodSymbol symbol, + MethodDeclarationSyntax syntax, + FungeSpace space, + ExecutionBinding binding, + string? projDir, + string sourcePath) + { + var ns = symbol.ContainingType.ContainingNamespace?.IsGlobalNamespace == false + ? symbol.ContainingType.ContainingNamespace.ToDisplayString() : null; + + var typeKeyword = symbol.ContainingType.TypeKind switch + { + TypeKind.Struct when symbol.ContainingType.IsRecord => "record struct", + TypeKind.Struct => "struct", + TypeKind.Interface => "interface", + TypeKind.Class when symbol.ContainingType.IsRecord => "record", + _ => "class", + }; + + 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 returnTypeSyntax = symbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + var paramList = string.Join(", ", System.Linq.Enumerable.Select(symbol.Parameters, p => + { + var prefix = (binding.ReturnKind == ReturnKind.AsyncEnumerableByte + && binding.CancellationTokenName is not null + && string.Equals(p.Name, binding.CancellationTokenName, StringComparison.Ordinal)) + ? "[global::System.Runtime.CompilerServices.EnumeratorCancellation] " + : string.Empty; + return prefix + p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + " " + p.Name; + })); + + var relPath = projDir is not null ? MakeRelative(projDir, sourcePath) : sourcePath; + var sb = new StringBuilder(); + + if (ns is not null) { sb.Append("namespace ").Append(ns).AppendLine(" {").AppendLine(); } + sb.Append("partial ").Append(typeKeyword).Append(' ').AppendLine(typeName); + sb.AppendLine("{"); + sb.AppendLine($" // Generated from: {relPath}"); + sb.Append(" ").Append(accessibility).Append(staticMod).Append(asyncMod) + .Append(" partial ").Append(returnTypeSyntax).Append(' ') + .Append(symbol.Name).Append('(').Append(paramList).AppendLine(")"); + sb.AppendLine(" {"); + + EmitBody(sb, space, binding); + + sb.AppendLine(" }"); + sb.AppendLine("}"); + if (ns is not null) sb.AppendLine("}"); + return sb.ToString(); + } + + static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding 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())", + _ => "global::System.IO.TextReader.Null", + }; + + EmitSpaceData(sb, space); + + switch (binding.ReturnKind) + { + case ReturnKind.Int: + case ReturnKind.String: + case ReturnKind.TaskInt: + case ReturnKind.TaskString: + case ReturnKind.ValueTaskInt: + case ReturnKind.ValueTaskString: + case ReturnKind.EnumerableByte: + case ReturnKind.AsyncEnumerableByte: + sb.AppendLine(" var __fungeOutput = new global::System.IO.StringWriter();"); + if (binding.ReturnKind is ReturnKind.Int or ReturnKind.TaskInt or ReturnKind.ValueTaskInt) + { + sb.AppendLine(" var __fungeExitCode = global::Esolang.Funge.__Generated.FungeRuntime.Run("); + sb.AppendLine($" __cells, __minX, __minY, __maxX, __maxY, {inputExpr}, __fungeOutput, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")});"); + } + else + { + EmitRuntimeRunCall(sb, inputExpr, "__fungeOutput", binding.HasExplicitInput, binding.HasExplicitOutput); + } + break; + case ReturnKind.Void: + case ReturnKind.Task: + case ReturnKind.ValueTask: + { + if (binding.OutputKind == OutputKind.PipeWriter) + { + sb.AppendLine($" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true);"); + EmitRuntimeRunCall(sb, inputExpr, "__fungeOutput", binding.HasExplicitInput, binding.HasExplicitOutput); + } + else + { + var outExpr = binding.OutputKind == OutputKind.TextWriter + ? binding.OutputExpression + : "global::System.IO.TextWriter.Null"; + EmitRuntimeRunCall(sb, inputExpr, outExpr, binding.HasExplicitInput, binding.HasExplicitOutput); + } + break; + } + } + + switch (binding.ReturnKind) + { + case ReturnKind.Int: + sb.AppendLine(" return __fungeExitCode;"); + break; + case ReturnKind.String: + sb.AppendLine(" return __fungeOutput.ToString();"); + break; + case ReturnKind.Task: + sb.AppendLine(" return global::System.Threading.Tasks.Task.CompletedTask;"); + break; + case ReturnKind.TaskInt: + sb.AppendLine(" return global::System.Threading.Tasks.Task.FromResult(__fungeExitCode);"); + break; + case ReturnKind.TaskString: + sb.AppendLine(" return global::System.Threading.Tasks.Task.FromResult(__fungeOutput.ToString());"); + break; + case ReturnKind.ValueTask: + sb.AppendLine(" return default(global::System.Threading.Tasks.ValueTask);"); + break; + case ReturnKind.ValueTaskInt: + sb.AppendLine(" return new global::System.Threading.Tasks.ValueTask(__fungeExitCode);"); + break; + case ReturnKind.ValueTaskString: + sb.AppendLine(" return new global::System.Threading.Tasks.ValueTask(__fungeOutput.ToString());"); + break; + case ReturnKind.EnumerableByte: + case ReturnKind.AsyncEnumerableByte: + sb.AppendLine(" foreach (var __b in global::System.Text.Encoding.UTF8.GetBytes(__fungeOutput.ToString()))"); + sb.AppendLine(" yield return __b;"); + break; + } + } + + static void EmitRuntimeRunCall(StringBuilder sb, string inputExpr, string outputExpr, bool hasInput, bool hasOutput) + { + sb.AppendLine(" global::Esolang.Funge.__Generated.FungeRuntime.Run("); + sb.AppendLine($" __cells, __minX, __minY, __maxX, __maxY, {inputExpr}, {outputExpr}, {(hasInput ? "true" : "false")}, {(hasOutput ? "true" : "false")});"); + } + + static void EmitSpaceData(StringBuilder sb, FungeSpace space) + { + sb.AppendLine($" int __minX = {space.MinX}, __minY = {space.MinY}, __maxX = {space.MaxX}, __maxY = {space.MaxY};"); + sb.AppendLine(" var __cells = new global::System.Collections.Generic.Dictionary<(int, int), int>();"); + for (var y = space.MinY; y <= space.MaxY; y++) + for (var x = space.MinX; x <= space.MaxX; x++) + { + var val = space[new FungeVector(x, y)]; + if (val != ' ') + sb.AppendLine($" __cells[({x}, {y})] = {val};"); + } + } + + // ----------------------------------------------------------------------- + // I/O scan + // ----------------------------------------------------------------------- + + static (bool usesOutput, bool usesInput) ScanFungeIo(FungeSpace space) + { + bool usesOutput = false, usesInput = false; + for (var y = space.MinY; y <= space.MaxY; y++) + for (var x = space.MinX; x <= space.MaxX; x++) + { + var c = space[new FungeVector(x, y)]; + if (c is '.' or ',') usesOutput = true; + if (c is '&' or '~') usesInput = true; + if (usesOutput && usesInput) return (true, true); + } + return (usesOutput, usesInput); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + static string GetAccessibility(Accessibility accessibility) => accessibility switch + { + Accessibility.Public => "public", + Accessibility.Protected => "protected", + Accessibility.Internal => "internal", + Accessibility.Private => "private", + Accessibility.ProtectedAndInternal => "private protected", + Accessibility.ProtectedOrInternal => "protected internal", + _ => string.Empty, + }; + + static bool IsLanguageVersionAtLeastCSharp8(LanguageVersion v) => v switch + { + LanguageVersion.Default => true, + LanguageVersion.Latest => true, + LanguageVersion.Preview => true, + LanguageVersion.LatestMajor => true, + _ => v >= LanguageVersion.CSharp8, + }; + + static string NormalizePath(string path) => path.Replace('\\', '/').TrimStart('/'); + + 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; + } +} diff --git a/Generator/README.md b/Generator/README.md index dd449cb..f9a953f 100644 --- a/Generator/README.md +++ b/Generator/README.md @@ -12,10 +12,13 @@ The generator reads the Funge-98 source (from a file or inline) and emits a comp | Return type | Description | |---|---| | `void` | Run to completion; discard output | +| `int` | Return program exit code (`q` pops stack top, otherwise `0`) | | `string` | Collect all output and return as a string | | `Task` | Async run; discard output | +| `Task` | Async run; return program exit code | | `Task` | Async run; return output string | | `ValueTask` | Async run; discard output | +| `ValueTask` | Async run; return program exit code | | `ValueTask` | Async run; return output string | | `IEnumerable` | Yield output bytes synchronously | | `IAsyncEnumerable` | Yield output bytes asynchronously | @@ -115,7 +118,7 @@ sufficient for programs that do not rely on concurrency, stack stack operations, | Storage (self-modifying) | `g` `p` | 🟡 storage offset not applied | | I/O | `.` `,` `&` `~` | ✅ | | Misc | `z` `@` | ✅ | -| Exit code | `q` | 🟡 terminates normally but exit code is discarded | +| Exit code | `q` | ✅ pops stack top and returns it as method exit code (`@` returns `0`) | | Iteration | `k` | ✅ | | Concurrency | `t` | ❌ not implemented (single IP only) | | Stack stack | `{` `}` `u` | ❌ not implemented | From 740c1e767b23eda23217c1f2feaf1b053efa60e7 Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 7 May 2026 18:15:26 +0900 Subject: [PATCH 04/17] Use Esolang.Processor.Abstractions for processor interfaces --- CHANGELOG.md | 3 +- Directory.Build.targets | 1 + Processor/Esolang.Funge.Processor.csproj | 1 + Processor/IProcessor.cs | 55 ------------------------ 4 files changed, 4 insertions(+), 56 deletions(-) delete mode 100644 Processor/IProcessor.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a12416..ac5acb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,13 @@ The format is based on Keep a Changelog. ### Added -- `Esolang.Funge.Processor/Processor/IProcessor.cs`: provisional execution abstractions under `Esolang.Processor` (`IProcessor`, `ITextProcessor`, `IPipeProcessor`) for later extraction to a shared package. +- `Esolang.Processor.Abstractions` (`Esolang.Processor` namespace): shared execution abstractions package (`IProcessor`, `ITextProcessor`, `IPipeProcessor`). - `Esolang.Funge.Processor.Tests`: coverage for `RunToEnd(...)` and `RunToEndAsync(...)` on `FungeProcessor`. ### Changed - `Esolang.Funge.Processor`: `FungeProcessor` now implements `ITextProcessor` and exposes `RunToEnd(...)` / `RunToEndAsync(...)` while preserving existing `Run(...)` behavior. +- `Esolang.Funge.Processor`: switched abstraction source from local `Processor/IProcessor.cs` to `Esolang.Processor.Abstractions` package. - `dotnet-funge` (`Esolang.Funge.Interpreter`): command execution path now calls `RunToEnd(...)`. - `Esolang.Funge.Generator`: added return-type support for `int`, `Task`, and `ValueTask`, and aligned `q` handling to return the popped exit code (`@` returns `0`). diff --git a/Directory.Build.targets b/Directory.Build.targets index 1a96e0f..aaba90e 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -7,6 +7,7 @@ + diff --git a/Processor/Esolang.Funge.Processor.csproj b/Processor/Esolang.Funge.Processor.csproj index 1177d80..3c44c82 100644 --- a/Processor/Esolang.Funge.Processor.csproj +++ b/Processor/Esolang.Funge.Processor.csproj @@ -21,6 +21,7 @@ + diff --git a/Processor/IProcessor.cs b/Processor/IProcessor.cs deleted file mode 100644 index c47d91d..0000000 --- a/Processor/IProcessor.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.IO.Pipelines; - -#pragma warning disable IDE0130 // Namespace does not match folder structure -namespace Esolang.Processor; -#pragma warning restore IDE0130 - -// TODO: 将来的に Esolang.Processor.Abstractions パッケージへ切り出し予定 - -/// -/// Processor の共通基底。実行対象のプログラムを保持する。 -/// -/// パース済みプログラムの型。 -public interface IProcessor -{ - /// パース済みプログラム。 - TProgram Program { get; } -} - -/// -/// / ベースの実行 IF。 -/// -/// パース済みプログラムの型。 -public interface ITextProcessor : IProcessor -{ - /// プログラムを最後まで実行し、終了コードを返す。 - int RunToEnd( - TextReader? input = null, - TextWriter? output = null, - CancellationToken cancellationToken = default); - - /// プログラムを最後まで非同期実行し、終了コードを返す。 - ValueTask RunToEndAsync( - TextReader? input = null, - TextWriter? output = null, - CancellationToken cancellationToken = default); -} - -/// -/// / ベースの実行 IF。 -/// -/// パース済みプログラムの型。 -public interface IPipeProcessor : IProcessor -{ - /// プログラムを最後まで実行し、終了コードを返す。 - int RunToEnd( - PipeReader input, - PipeWriter output, - CancellationToken cancellationToken = default); - - /// プログラムを最後まで非同期実行し、終了コードを返す。 - ValueTask RunToEndAsync( - PipeReader input, - PipeWriter output, - CancellationToken cancellationToken = default); -} From 251e9895ad9e50f42c61fbee4e9d9b95b6ccf3f2 Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 7 May 2026 23:46:20 +0900 Subject: [PATCH 05/17] feat: add Trefunge 3D support across parser/processor/generator Implement XYZ space, h/l/m directions, 3D get/put/x semantics, and layer parsing via form-feed. Update tests, compliance docs, root README status, changelog, and add 3D sample program. --- CHANGELOG.md | 6 + Generator.Tests/FungeMethodGeneratorTests.cs | 131 +++++++++++++++++- Generator/MethodGenerator.Runtime.cs | 113 ++++++++------- Generator/MethodGenerator.cs | 40 +++--- Generator/README.md | 36 ++++- Interpreter/README.md | 6 +- Parser.Tests/FungeParserTests.cs | 33 ++++- Parser/FungeParser.cs | 7 +- Parser/FungeSpace.cs | 22 ++- Parser/FungeVector.cs | 41 ++++-- Parser/README.md | 4 +- Processor.Tests/FungeProcessorTests.cs | 35 ++++- Processor/FungeProcessor.cs | 66 +++++---- Processor/README.md | 8 +- README.md | 20 +++ .../Esolang.Funge.Generator.UseConsole.cs | 7 + .../Generator.UseConsole/Programs/hello3d.b98 | 1 + samples/Generator.UseConsole/README.md | 5 +- 18 files changed, 438 insertions(+), 143 deletions(-) create mode 100644 samples/Generator.UseConsole/Programs/hello3d.b98 diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5acb3..c129729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on Keep a Changelog. - `Esolang.Processor.Abstractions` (`Esolang.Processor` namespace): shared execution abstractions package (`IProcessor`, `ITextProcessor`, `IPipeProcessor`). - `Esolang.Funge.Processor.Tests`: coverage for `RunToEnd(...)` and `RunToEndAsync(...)` on `FungeProcessor`. +- `Esolang.Funge.Generator.Tests`: 3D runtime coverage for `h` / `l` / `m` and 3D `g` / `p` behavior. +- `samples/Generator.UseConsole/Programs/hello3d.b98`: minimal Trefunge sample using form-feed (`\f`) layer separation. ### Changed @@ -17,6 +19,10 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Processor`: switched abstraction source from local `Processor/IProcessor.cs` to `Esolang.Processor.Abstractions` package. - `dotnet-funge` (`Esolang.Funge.Interpreter`): command execution path now calls `RunToEnd(...)`. - `Esolang.Funge.Generator`: added return-type support for `int`, `Task`, and `ValueTask`, and aligned `q` handling to return the popped exit code (`@` returns `0`). +- `Esolang.Funge.Parser`: parser and space model now support 3D coordinates (`X`, `Y`, `Z`), with form-feed (`\f`) treated as a Z-layer separator. +- `Esolang.Funge.Processor`: enabled Trefunge 3D navigation instructions `h` / `l` / `m`, extended `?` to 6 directions, and upgraded `g` / `p` / `x` to 3D operands. +- `Esolang.Funge.Generator`: generated runtime now uses 3D execution space (XYZ bounds/cells), supports `h` / `l` / `m`, and handles 3D `g` / `p` / `x` semantics. +- Docs: updated package README compliance tables and added 3D notes/examples (including `\f` layer separator guidance). ## [1.0.1] - 2026-05-07 diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index caff3c4..99685b9 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -542,6 +542,125 @@ await Task.Factory.StartNew(() => }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } + [TestMethod] + public async Task Runtime_3D_GoLow_ExitCode() + { + const string program = "l\f>7q"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("go-low.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("go-low.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (int?)m.Invoke(null, []); + Assert.AreEqual(7, result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + [TestMethod] + public async Task Runtime_3D_GoHigh_ExitCode() + { + const string program = "h\f\f>7q"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("go-high.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("go-high.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (int?)m.Invoke(null, []); + Assert.AreEqual(7, result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + [TestMethod] + public async Task Runtime_3D_HighLowIf_SelectsDirection() + { + const string programLow = "0m\f >1q\f >2q"; + const string programHigh = "1m\f >1q\f >2q"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("select-low.b98")] + public static partial int RunLow(); + + [GenerateFungeMethod("select-high.b98")] + public static partial int RunHigh(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("select-low.b98", programLow), ("select-high.b98", programHigh)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var mLow = t.GetMethod("RunLow")!; + var mHigh = t.GetMethod("RunHigh")!; + var low = (int?)mLow.Invoke(null, []); + var high = (int?)mHigh.Invoke(null, []); + Assert.AreEqual(1, low); + Assert.AreEqual(2, high); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + [TestMethod] + public async Task Runtime_3D_GetPut_UsesXYZ() + { + const string program = "88*1+500p500gq"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("getput-3d.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("getput-3d.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (int?)m.Invoke(null, []); + Assert.AreEqual(65, result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + [TestMethod] public void Diagnostic_SourceFileNotFound_FG0004() { @@ -680,7 +799,7 @@ partial class TestClass var generated = comp.SyntaxTrees .Select(static t => t.ToString()) .Single(static text => text.Contains("Generated from: ", StringComparison.Ordinal)); - Assert.Contains("__cells[(0, 0)] = 64;", generated); + Assert.Contains("__cells[(0, 0, 0)] = 64;", generated); // Output all generated syntax trees for inspection TestContext.WriteLine("=== Generated Syntax Trees ==="); @@ -694,7 +813,7 @@ partial class TestClass [TestMethod] public void InlineSource_MultiLine_BasicProgram() { - // Verify multiline raw string is mapped to 2D coordinates as expected. + // Verify multiline raw string is mapped to X/Y at Z=0 as expected. var source = """" using Esolang.Funge; namespace TestProject; @@ -713,10 +832,10 @@ partial class TestClass var generated = comp.SyntaxTrees .Select(static t => t.ToString()) .Single(static text => text.Contains("Generated from: ", StringComparison.Ordinal)); - Assert.Contains("__cells[(0, 0)] = 62;", generated); // '>' - Assert.Contains("__cells[(1, 0)] = 118;", generated); // 'v' - Assert.Contains("__cells[(0, 1)] = 94;", generated); // '^' - Assert.Contains("__cells[(1, 1)] = 64;", generated); // '@' + Assert.Contains("__cells[(0, 0, 0)] = 62;", generated); // '>' + Assert.Contains("__cells[(1, 0, 0)] = 118;", generated); // 'v' + Assert.Contains("__cells[(0, 1, 0)] = 94;", generated); // '^' + Assert.Contains("__cells[(1, 1, 0)] = 64;", generated); // '@' } [TestMethod] diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index 7611243..d760122 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -25,38 +25,40 @@ namespace Esolang.Funge.__Generated internal static class FungeRuntime { internal static int Run( - Dictionary<(int, int), int> cells, - int minX, int minY, int maxX, int maxY, + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, TextReader input, TextWriter output, bool hasInput, bool hasOutput) { - int px = 0, py = 0, dx = 1, dy = 0; + int px = 0, py = 0, pz = 0, dx = 1, dy = 0, dz = 0; bool stringMode = false; var stack = new List(); var rng = new Random(); int exitCode = 0; - int GetCell(int x, int y) => cells.TryGetValue((x, y), out var v) ? v : ' '; - void SetCell(int x, int y, int val) + int GetCell(int x, int y, int z) => cells.TryGetValue((x, y, z), out var v) ? v : ' '; + void SetCell(int x, int y, int z, int val) { - if (val == ' ') cells.Remove((x, y)); - else cells[(x, y)] = val; + if (val == ' ') cells.Remove((x, y, z)); + else cells[(x, y, z)] = val; } int Pop() { if (stack.Count == 0) return 0; var v = stack[stack.Count - 1]; stack.RemoveAt(stack.Count - 1); return v; } void Push(int v) => stack.Add(v); - (int nx, int ny) Advance(int x, int y, int ddx, int ddy) + (int nx, int ny, int nz) Advance(int x, int y, int z, int ddx, int ddy, int ddz) { - if (maxX < minX) return (x, y); - int nx = x + ddx, ny = y + ddy; - int w = maxX - minX + 1, h = maxY - minY + 1; + if (maxX < minX) return (x, y, z); + int nx = x + ddx, ny = y + ddy, nz = z + ddz; + int w = maxX - minX + 1, h = maxY - minY + 1, d = maxZ - minZ + 1; if (nx < minX) nx = maxX - ((minX - nx - 1) % w); else if (nx > maxX) nx = minX + ((nx - maxX - 1) % w); if (ny < minY) ny = maxY - ((minY - ny - 1) % h); else if (ny > maxY) ny = minY + ((ny - maxY - 1) % h); - return (nx, ny); + if (nz < minZ) nz = maxZ - ((minZ - nz - 1) % d); + else if (nz > maxZ) nz = minZ + ((nz - maxZ - 1) % d); + return (nx, ny, nz); } bool IsSgmlSpace(int c) => c is ' ' or '\t' or '\f' or '\v'; @@ -83,36 +85,47 @@ void ExecuteInstruction(int cell, ref bool suppressAdvance) case '5': case '6': case '7': case '8': case '9': Push(cell - '0'); break; case 'a': Push(10); break; case 'b': Push(11); break; case 'c': Push(12); break; case 'd': Push(13); break; case 'e': Push(14); break; case 'f': Push(15); break; - case '>': dx = 1; dy = 0; break; - case '<': dx = -1; dy = 0; break; - case '^': dx = 0; dy = -1; break; - case 'v': dx = 0; dy = 1; break; + case '>': dx = 1; dy = 0; dz = 0; break; + case '<': dx = -1; dy = 0; dz = 0; break; + case '^': dx = 0; dy = -1; dz = 0; break; + case 'v': dx = 0; dy = 1; dz = 0; break; + case 'h': dx = 0; dy = 0; dz = -1; break; + case 'l': dx = 0; dy = 0; dz = 1; break; case '?': - switch (rng.Next(4)) { case 0: dx=1;dy=0;break; case 1:dx=-1;dy=0;break; case 2:dx=0;dy=-1;break; default:dx=0;dy=1;break; } + switch (rng.Next(6)) + { + case 0: dx=1;dy=0;dz=0;break; + case 1: dx=-1;dy=0;dz=0;break; + case 2: dx=0;dy=-1;dz=0;break; + case 3: dx=0;dy=1;dz=0;break; + case 4: dx=0;dy=0;dz=-1;break; + default: dx=0;dy=0;dz=1;break; + } break; - case '_': { int v = Pop(); dx = v == 0 ? 1 : -1; dy = 0; break; } - case '|': { int v = Pop(); dy = v == 0 ? 1 : -1; dx = 0; break; } - case '[': { int ndx = dy, ndy = -dx; dx = ndx; dy = ndy; break; } - case ']': { int ndx = -dy, ndy = dx; dx = ndx; dy = ndy; break; } - case 'r': dx = -dx; dy = -dy; break; - case 'x': { int ndy = Pop(), ndx = Pop(); dx = ndx; dy = ndy; break; } - case 'w': { int b = Pop(), a = Pop(); if(a>b){int ndx=-dy,ndy=dx;dx=ndx;dy=ndy;}else if(ab){int ndx=-dy,ndy=dx;dx=ndx;dy=ndy;dz=0;}else if(a= 0 ? dx : -dx, jdy = s >= 0 ? dy : -dy, abs = s < 0 ? -s : s; - for (int i = 0; i < abs; i++) (px, py) = Advance(px, py, jdx, jdy); + int s = Pop(); int jdx = s >= 0 ? dx : -dx, jdy = s >= 0 ? dy : -dy, jdz = s >= 0 ? dz : -dz, abs = s < 0 ? -s : s; + for (int i = 0; i < abs; i++) (px, py, pz) = Advance(px, py, pz, jdx, jdy, jdz); suppressAdvance = true; break; } case ';': - (px, py) = Advance(px, py, dx, dy); - while (GetCell(px, py) != ';') (px, py) = Advance(px, py, dx, dy); + (px, py, pz) = Advance(px, py, pz, dx, dy, dz); + while (GetCell(px, py, pz) != ';') (px, py, pz) = Advance(px, py, pz, dx, dy, dz); break; - case '\'': (px, py) = Advance(px, py, dx, dy); Push(GetCell(px, py)); break; - case 's': { int sv = Pop(); (px, py) = Advance(px, py, dx, dy); SetCell(px, py, sv); break; } + case '\'': (px, py, pz) = Advance(px, py, pz, dx, dy, dz); Push(GetCell(px, py, pz)); break; + case 's': { int sv = Pop(); (px, py, pz) = Advance(px, py, pz, dx, dy, dz); SetCell(px, py, pz, sv); break; } case '"': stringMode = true; break; - case 'g': { int gy = Pop(), gx = Pop(); Push(GetCell(gx, gy)); break; } - case 'p': { int gy = Pop(), gx = Pop(), pv = Pop(); SetCell(gx, gy, pv); break; } + case 'g': { int gz = Pop(), gy = Pop(), gx = Pop(); Push(GetCell(gx, gy, gz)); break; } + case 'p': { int gz = Pop(), gy = Pop(), gx = Pop(), pv = Pop(); SetCell(gx, gy, gz, pv); break; } case '.': if (!hasOutput) throw new InvalidOperationException("Funge output instruction '.' executed without an output interface."); output.Write(Pop()); output.Write(' '); break; @@ -122,29 +135,29 @@ void ExecuteInstruction(int cell, ref bool suppressAdvance) case '&': { if (!hasInput) throw new InvalidOperationException("Funge input instruction '&' executed without an input interface."); - var line = input.ReadLine(); if(line==null){dx=-dx;dy=-dy;}else Push(int.TryParse(line.Trim(),out int iv)?iv:0); break; + var line = input.ReadLine(); if(line==null){dx=-dx;dy=-dy;dz=-dz;}else Push(int.TryParse(line.Trim(),out int iv)?iv:0); break; } case '~': { if (!hasInput) throw new InvalidOperationException("Funge input instruction '~' executed without an input interface."); - int ch = input.Read(); if(ch<0){dx=-dx;dy=-dy;}else Push(ch); break; + int ch = input.Read(); if(ch<0){dx=-dx;dy=-dy;dz=-dz;}else Push(ch); break; } case 'k': { int n = Pop(); - var (ix, iy) = Advance(px, py, dx, dy); + var (ix, iy, iz) = Advance(px, py, pz, dx, dy, dz); while (true) { - int c = GetCell(ix, iy); + int c = GetCell(ix, iy, iz); if (IsSgmlSpace(c)) { - (ix, iy) = Advance(ix, iy, dx, dy); + (ix, iy, iz) = Advance(ix, iy, iz, dx, dy, dz); } else if (c == ';') { - (ix, iy) = Advance(ix, iy, dx, dy); - while (GetCell(ix, iy) != ';') (ix, iy) = Advance(ix, iy, dx, dy); - (ix, iy) = Advance(ix, iy, dx, dy); + (ix, iy, iz) = Advance(ix, iy, iz, dx, dy, dz); + while (GetCell(ix, iy, iz) != ';') (ix, iy, iz) = Advance(ix, iy, iz, dx, dy, dz); + (ix, iy, iz) = Advance(ix, iy, iz, dx, dy, dz); } else { @@ -154,11 +167,11 @@ void ExecuteInstruction(int cell, ref bool suppressAdvance) if (n == 0) { - (px, py) = (ix, iy); + (px, py, pz) = (ix, iy, iz); } else { - int operand = GetCell(ix, iy); + int operand = GetCell(ix, iy, iz); for (int i = 0; i < n && !stopped; i++) { bool dummy = false; @@ -169,13 +182,13 @@ void ExecuteInstruction(int cell, ref bool suppressAdvance) } case '@': stopped = true; break; case 'q': exitCode = Pop(); stopped = true; break; - default: if (cell >= 'A' && cell <= 'Z') { dx = -dx; dy = -dy; } break; + default: if (cell >= 'A' && cell <= 'Z') { dx = -dx; dy = -dy; dz = -dz; } break; } } while (!stopped) { - int cell = GetCell(px, py); + int cell = GetCell(px, py, pz); if (stringMode) { if (cell == '"') @@ -187,10 +200,10 @@ void ExecuteInstruction(int cell, ref bool suppressAdvance) Push(' '); while (true) { - var (nx, ny) = Advance(px, py, dx, dy); - if (IsSgmlSpace(GetCell(nx, ny))) + var (nx, ny, nz) = Advance(px, py, pz, dx, dy, dz); + if (IsSgmlSpace(GetCell(nx, ny, nz))) { - (px, py) = (nx, ny); + (px, py, pz) = (nx, ny, nz); } else { @@ -202,13 +215,13 @@ void ExecuteInstruction(int cell, ref bool suppressAdvance) { Push(cell); } - (px, py) = Advance(px, py, dx, dy); + (px, py, pz) = Advance(px, py, pz, dx, dy, dz); continue; } bool suppressAdvance = false; ExecuteInstruction(cell, ref suppressAdvance); - if (!stopped && !suppressAdvance) (px, py) = Advance(px, py, dx, dy); + if (!stopped && !suppressAdvance) (px, py, pz) = Advance(px, py, pz, dx, dy, dz); } return exitCode; diff --git a/Generator/MethodGenerator.cs b/Generator/MethodGenerator.cs index b84149d..74613d5 100644 --- a/Generator/MethodGenerator.cs +++ b/Generator/MethodGenerator.cs @@ -510,7 +510,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin if (binding.ReturnKind is ReturnKind.Int or ReturnKind.TaskInt or ReturnKind.ValueTaskInt) { sb.AppendLine(" var __fungeExitCode = global::Esolang.Funge.__Generated.FungeRuntime.Run("); - sb.AppendLine($" __cells, __minX, __minY, __maxX, __maxY, {inputExpr}, __fungeOutput, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")});"); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, __fungeOutput, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")});"); } else { @@ -574,20 +574,21 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin static void EmitRuntimeRunCall(StringBuilder sb, string inputExpr, string outputExpr, bool hasInput, bool hasOutput) { sb.AppendLine(" global::Esolang.Funge.__Generated.FungeRuntime.Run("); - sb.AppendLine($" __cells, __minX, __minY, __maxX, __maxY, {inputExpr}, {outputExpr}, {(hasInput ? "true" : "false")}, {(hasOutput ? "true" : "false")});"); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {outputExpr}, {(hasInput ? "true" : "false")}, {(hasOutput ? "true" : "false")});"); } static void EmitSpaceData(StringBuilder sb, FungeSpace space) { - sb.AppendLine($" int __minX = {space.MinX}, __minY = {space.MinY}, __maxX = {space.MaxX}, __maxY = {space.MaxY};"); - sb.AppendLine(" var __cells = new global::System.Collections.Generic.Dictionary<(int, int), int>();"); - for (var y = space.MinY; y <= space.MaxY; y++) - for (var x = space.MinX; x <= space.MaxX; x++) - { - var val = space[new FungeVector(x, y)]; - if (val != ' ') - sb.AppendLine($" __cells[({x}, {y})] = {val};"); - } + sb.AppendLine($" int __minX = {space.MinX}, __minY = {space.MinY}, __minZ = {space.MinZ}, __maxX = {space.MaxX}, __maxY = {space.MaxY}, __maxZ = {space.MaxZ};"); + sb.AppendLine(" var __cells = new global::System.Collections.Generic.Dictionary<(int, int, int), int>();"); + for (var z = space.MinZ; z <= space.MaxZ; z++) + for (var y = space.MinY; y <= space.MaxY; y++) + for (var x = space.MinX; x <= space.MaxX; x++) + { + var val = space[new FungeVector(x, y, z)]; + if (val != ' ') + sb.AppendLine($" __cells[({x}, {y}, {z})] = {val};"); + } } // ----------------------------------------------------------------------- @@ -597,14 +598,15 @@ static void EmitSpaceData(StringBuilder sb, FungeSpace space) static (bool usesOutput, bool usesInput) ScanFungeIo(FungeSpace space) { bool usesOutput = false, usesInput = false; - for (var y = space.MinY; y <= space.MaxY; y++) - for (var x = space.MinX; x <= space.MaxX; x++) - { - var c = space[new FungeVector(x, y)]; - if (c is '.' or ',') usesOutput = true; - if (c is '&' or '~') usesInput = true; - if (usesOutput && usesInput) return (true, true); - } + for (var z = space.MinZ; z <= space.MaxZ; z++) + for (var y = space.MinY; y <= space.MaxY; y++) + for (var x = space.MinX; x <= space.MaxX; x++) + { + var c = space[new FungeVector(x, y, z)]; + if (c is '.' or ',') usesOutput = true; + if (c is '&' or '~') usesInput = true; + if (usesOutput && usesInput) return (true, true); + } return (usesOutput, usesInput); } diff --git a/Generator/README.md b/Generator/README.md index f9a953f..b9db944 100644 --- a/Generator/README.md +++ b/Generator/README.md @@ -86,6 +86,33 @@ Funge-98 code can be embedded directly as a string literal using `InlineSource` public static partial string HelloWorldInline(); ``` +## 3D (Trefunge) support + +The generator fully supports 3D Funge-98 (Trefunge) programs. +Within a source file, the form-feed character (`\f`, U+000C) separates Z-layers. + +``` +# Layer Z=0 — jump into layer Z=1 +l +``` + +*(form-feed character here separates layers)* + +``` +# Layer Z=1 — runs the Hello World program +>64+"!dlroW ,olleH">:#,_@ +``` + +**3D direction instructions:** + +| Instruction | Description | +|---|---| +| `h` | Set delta to High `(0, 0, −1)` | +| `l` | Set delta to Low `(0, 0, +1)` | +| `m` | Pop value; zero → Low, non-zero → High | + +The generated runtime automatically handles XYZ coordinates, Z-axis wrapping, and 3D `g`/`p`/`x` operands. + ## Diagnostics | ID | Severity | Description | @@ -103,15 +130,15 @@ public static partial string HelloWorldInline(); ## Funge-98 Compliance -The generated runtime (`FungeRuntime`) implements a **single-IP, single-stack 2D subset** of Befunge-98, -sufficient for programs that do not rely on concurrency, stack stack operations, or fingerprints. +The generated runtime (`FungeRuntime`) implements a **single-IP, single-stack subset** of Funge-98, +including Trefunge 3D navigation (`h` / `l` / `m`) but excluding concurrency, stack stack operations, and fingerprints. | Category | Instructions | Status | |---|---|---| | Stack | `0`–`9` `a`–`f` `:` `$` `\` `n` | ✅ | | Arithmetic | `+` `-` `*` `/` `%` | ✅ | | Comparison | `` ` `` `!` | ✅ | -| Direction | `>` `<` `^` `v` `?` `[` `]` `r` `x` `w` | ✅ | +| Direction | `>` `<` `^` `v` `h` `l` `?` `[` `]` `r` `x` `m` `w` | ✅ | | Branching | `_` `\|` | ✅ | | Movement | `#` `;` `j` | ✅ | | String / char | `"` `'` `s` | ✅ (stringmode contiguous spaces are SGML-style) | @@ -126,7 +153,8 @@ sufficient for programs that do not rely on concurrency, stack stack operations, | File I/O | `i` `o` | ❌ not implemented | | System exec | `=` | ❌ not implemented | | Fingerprints | `(` `)` `A`–`Z` | ❌ reflects (not implemented) | -| 3D (Trefunge) | `h` `l` `m` | ❌ not implemented (2D only) | +| 3D (Trefunge) | `h` `l` `m` | ✅ | +| ND-generalized space | dimensions > 3 | ❌ not implemented | ## References diff --git a/Interpreter/README.md b/Interpreter/README.md index 8c985e2..ba95c71 100644 --- a/Interpreter/README.md +++ b/Interpreter/README.md @@ -1,6 +1,6 @@ # dotnet-funge -Command-line interpreter for [Funge-98](https://github.com/catseye/Funge-98/blob/master/doc/funge98.markdown) (Befunge-98) programs. +Command-line interpreter for [Funge-98](https://github.com/catseye/Funge-98/blob/master/doc/funge98.markdown) programs. ## Installation @@ -30,7 +30,7 @@ The process exit code reflects the value passed to `q`; it is `0` if the program ## Funge-98 Compliance -Delegates execution to `Esolang.Funge.Processor`, which targets **Befunge-98** (2D). +Delegates execution to `Esolang.Funge.Processor`, including Trefunge 3D directions (`h`/`l`/`m`). For detailed processor-level behavior, refer to the processor package documentation. | Area | Status | @@ -43,7 +43,7 @@ For detailed processor-level behavior, refer to the processor package documentat | Fingerprints (`(` `)` `A`–`Z`) | ❌ reflects (not implemented) | | File I/O (`i` `o`) | ❌ reflects (not implemented) | | System exec (`=`) | ❌ reflects (not implemented) | -| 3D / Trefunge (`h` `l` `m`) | ❌ reflects (2D only) | +| 3D / Trefunge (`h` `l` `m`) | ✅ | ## References diff --git a/Parser.Tests/FungeParserTests.cs b/Parser.Tests/FungeParserTests.cs index 7fade70..9e75439 100644 --- a/Parser.Tests/FungeParserTests.cs +++ b/Parser.Tests/FungeParserTests.cs @@ -52,8 +52,10 @@ public void BoundingBox_CorrectAfterParse() var space = FungeParser.Parse("AB\nCD"); Assert.AreEqual(0, space.MinX); Assert.AreEqual(0, space.MinY); + Assert.AreEqual(0, space.MinZ); Assert.AreEqual(1, space.MaxX); Assert.AreEqual(1, space.MaxY); + Assert.AreEqual(0, space.MaxZ); } [TestMethod] @@ -64,17 +66,28 @@ public void BoundingBox_IncludesSpacesInSource() Assert.AreEqual(2, space.MaxX); Assert.AreEqual(0, space.MinY); Assert.AreEqual(0, space.MaxY); + Assert.AreEqual(0, space.MinZ); + Assert.AreEqual(0, space.MaxZ); } [TestMethod] public void Parse_SgmlSpaces_AreTreatedAsSpaceCells() { - var space = FungeParser.Parse("A\t\f\vB"); + var space = FungeParser.Parse("A\t\vB"); Assert.AreEqual('A', space[new FungeVector(0, 0)]); Assert.AreEqual(' ', space[new FungeVector(1, 0)]); Assert.AreEqual(' ', space[new FungeVector(2, 0)]); - Assert.AreEqual(' ', space[new FungeVector(3, 0)]); - Assert.AreEqual('B', space[new FungeVector(4, 0)]); + Assert.AreEqual('B', space[new FungeVector(3, 0)]); + } + + [TestMethod] + public void Parse_FormFeed_StartsNewLayer() + { + var space = FungeParser.Parse("A\fB"); + Assert.AreEqual('A', space[new FungeVector(0, 0, 0)]); + Assert.AreEqual('B', space[new FungeVector(0, 0, 1)]); + Assert.AreEqual(0, space.MinZ); + Assert.AreEqual(1, space.MaxZ); } } @@ -99,7 +112,7 @@ public void Reflect_EastBecomeWest() [TestMethod] public void Addition() - => Assert.AreEqual(new FungeVector(3, 5), new FungeVector(1, 2) + new FungeVector(2, 3)); + => Assert.AreEqual(new FungeVector(3, 5, 7), new FungeVector(1, 2, 3) + new FungeVector(2, 3, 4)); } [TestClass] @@ -135,10 +148,20 @@ public void Advance_WrapsSouthBeyondMaxY() public void SetCell_UpdatesBoundingBox() { var space = new FungeSpace(); - space[new FungeVector(5, 10)] = 'X'; + space[new FungeVector(5, 10, 15)] = 'X'; Assert.AreEqual(5, space.MinX); Assert.AreEqual(5, space.MaxX); Assert.AreEqual(10, space.MinY); Assert.AreEqual(10, space.MaxY); + Assert.AreEqual(15, space.MinZ); + Assert.AreEqual(15, space.MaxZ); + } + + [TestMethod] + public void Advance_WrapsLowBeyondMaxZ() + { + var space = FungeParser.Parse("A\fB"); + var next = space.Advance(new FungeVector(0, 0, 1), FungeVector.Low); + Assert.AreEqual(new FungeVector(0, 0, 0), next); } } diff --git a/Parser/FungeParser.cs b/Parser/FungeParser.cs index ddc9f20..37077e3 100644 --- a/Parser/FungeParser.cs +++ b/Parser/FungeParser.cs @@ -15,19 +15,20 @@ public static class FungeParser public static FungeSpace Parse(string source) { var space = new FungeSpace(); - int x = 0, y = 0; + int x = 0, y = 0, z = 0; foreach (var ch in source) { if (ch == '\r') continue; if (ch == '\n') { x = 0; y++; continue; } + if (ch == '\f') { x = 0; y = 0; z++; continue; } var cell = ch switch { - '\t' or '\f' or '\v' => ' ', + '\t' or '\v' => ' ', _ => ch, }; - var pos = new FungeVector(x, y); + var pos = new FungeVector(x, y, z); space.EnsureBounds(pos); if (cell != ' ') space[pos] = cell; diff --git a/Parser/FungeSpace.cs b/Parser/FungeSpace.cs index bec74b4..894b19a 100644 --- a/Parser/FungeSpace.cs +++ b/Parser/FungeSpace.cs @@ -1,13 +1,13 @@ namespace Esolang.Funge.Parser; /// -/// Represents the Funge-98 program space: a sparse, conceptually infinite 2D grid of integer cells. +/// Represents the Funge-98 program space: a sparse, conceptually infinite 3D grid of integer cells. /// Unset cells default to the space character (ASCII 32). /// public sealed class FungeSpace { private readonly Dictionary _cells = new(); - private int _minX, _minY, _maxX, _maxY; + private int _minX, _minY, _minZ, _maxX, _maxY, _maxZ; private bool _hasAny; private void IncludeInBounds(FungeVector pos) @@ -16,6 +16,7 @@ private void IncludeInBounds(FungeVector pos) { _minX = _maxX = pos.X; _minY = _maxY = pos.Y; + _minZ = _maxZ = pos.Z; _hasAny = true; return; } @@ -24,6 +25,8 @@ private void IncludeInBounds(FungeVector pos) if (pos.X > _maxX) _maxX = pos.X; if (pos.Y < _minY) _minY = pos.Y; if (pos.Y > _maxY) _maxY = pos.Y; + if (pos.Z < _minZ) _minZ = pos.Z; + if (pos.Z > _maxZ) _maxZ = pos.Z; } /// @@ -66,6 +69,12 @@ public int this[FungeVector pos] /// Maximum Y coordinate of the populated bounding box. public int MaxY => _maxY; + /// Minimum Z coordinate of the populated bounding box. + public int MinZ => _minZ; + + /// Maximum Z coordinate of the populated bounding box. + public int MaxZ => _maxZ; + /// /// Advances a position by , wrapping around the Least Significant /// Bounding Box (LSAB) when the result would leave it. @@ -80,9 +89,11 @@ public FungeVector Advance(FungeVector pos, FungeVector delta) var nextX = pos.X + delta.X; var nextY = pos.Y + delta.Y; + var nextZ = pos.Z + delta.Z; var width = _maxX - _minX + 1; var height = _maxY - _minY + 1; + var depth = _maxZ - _minZ + 1; if (nextX < _minX) nextX = _maxX - ((_minX - nextX - 1) % width); @@ -94,6 +105,11 @@ public FungeVector Advance(FungeVector pos, FungeVector delta) else if (nextY > _maxY) nextY = _minY + ((nextY - _maxY - 1) % height); - return new FungeVector(nextX, nextY); + if (nextZ < _minZ) + nextZ = _maxZ - ((_minZ - nextZ - 1) % depth); + else if (nextZ > _maxZ) + nextZ = _minZ + ((nextZ - _maxZ - 1) % depth); + + return new FungeVector(nextX, nextY, nextZ); } } diff --git a/Parser/FungeVector.cs b/Parser/FungeVector.cs index 982ad40..e5020bc 100644 --- a/Parser/FungeVector.cs +++ b/Parser/FungeVector.cs @@ -1,7 +1,7 @@ namespace Esolang.Funge.Parser; /// -/// Represents a 2D integer vector used for positions and deltas in Funge-98. +/// Represents a 3D integer vector used for positions and deltas in Funge-98. /// public readonly struct FungeVector : IEquatable { @@ -11,17 +11,28 @@ namespace Esolang.Funge.Parser; /// The Y component. public int Y { get; } + /// The Z component. + public int Z { get; } + /// Initializes a new with the given components. - public FungeVector(int x, int y) { X = x; Y = y; } + public FungeVector(int x, int y, int z) + { + X = x; + Y = y; + Z = z; + } + + /// Initializes a new in 2D (Z=0). + public FungeVector(int x, int y) : this(x, y, 0) { } /// - public bool Equals(FungeVector other) => X == other.X && Y == other.Y; + public bool Equals(FungeVector other) => X == other.X && Y == other.Y && Z == other.Z; /// public override bool Equals(object? obj) => obj is FungeVector v && Equals(v); /// - public override int GetHashCode() => HashCode.Combine(X, Y); + public override int GetHashCode() => HashCode.Combine(HashCode.Combine(X, Y), Z); /// Equality operator. public static bool operator ==(FungeVector left, FungeVector right) => left.Equals(right); @@ -30,7 +41,7 @@ namespace Esolang.Funge.Parser; public static bool operator !=(FungeVector left, FungeVector right) => !left.Equals(right); /// - public override string ToString() => $"({X}, {Y})"; + public override string ToString() => $"({X}, {Y}, {Z})"; /// Delta for East direction (right): (1, 0). public static readonly FungeVector East = new(1, 0); @@ -44,30 +55,36 @@ namespace Esolang.Funge.Parser; /// Delta for South direction (down): (0, 1). public static readonly FungeVector South = new(0, 1); + /// Delta for High direction (towards -Z): (0, 0, -1). + public static readonly FungeVector High = new(0, 0, -1); + + /// Delta for Low direction (towards +Z): (0, 0, 1). + public static readonly FungeVector Low = new(0, 0, 1); + /// Adds two vectors. - public static FungeVector operator +(FungeVector a, FungeVector b) => new(a.X + b.X, a.Y + b.Y); + public static FungeVector operator +(FungeVector a, FungeVector b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); /// Subtracts two vectors. - public static FungeVector operator -(FungeVector a, FungeVector b) => new(a.X - b.X, a.Y - b.Y); + public static FungeVector operator -(FungeVector a, FungeVector b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z); /// Negates a vector. - public static FungeVector operator -(FungeVector a) => new(-a.X, -a.Y); + public static FungeVector operator -(FungeVector a) => new(-a.X, -a.Y, -a.Z); /// Scales a vector by a scalar. - public static FungeVector operator *(FungeVector a, int scalar) => new(a.X * scalar, a.Y * scalar); + public static FungeVector operator *(FungeVector a, int scalar) => new(a.X * scalar, a.Y * scalar, a.Z * scalar); /// /// Rotates 90 degrees clockwise (Turn Right ]). /// - public FungeVector RotateRight() => new(-Y, X); + public FungeVector RotateRight() => new(-Y, X, Z); /// /// Rotates 90 degrees counter-clockwise (Turn Left [). /// - public FungeVector RotateLeft() => new(Y, -X); + public FungeVector RotateLeft() => new(Y, -X, Z); /// /// Reflects the vector, reversing direction (r). /// - public FungeVector Reflect() => new(-X, -Y); + public FungeVector Reflect() => new(-X, -Y, -Z); } diff --git a/Parser/README.md b/Parser/README.md index 4cf754b..4707813 100644 --- a/Parser/README.md +++ b/Parser/README.md @@ -8,8 +8,8 @@ This library provides the fundamental data structures for representing and manip | Type | Description | |---|---| -| `FungeVector` | Immutable 2D coordinate `(X, Y)` | -| `FungeSpace` | Sparse infinite 2D grid of integer cells (space = 32) | +| `FungeVector` | Immutable 3D coordinate `(X, Y, Z)` | +| `FungeSpace` | Sparse infinite 3D grid of integer cells (space = 32) | | `FungeParser` | Parses Funge-98 source text into a `FungeSpace` | ## Installation diff --git a/Processor.Tests/FungeProcessorTests.cs b/Processor.Tests/FungeProcessorTests.cs index 3959923..7e7eaa4 100644 --- a/Processor.Tests/FungeProcessorTests.cs +++ b/Processor.Tests/FungeProcessorTests.cs @@ -114,7 +114,7 @@ public void EastWestIf_Zero_GoesEast() #pragma warning restore IDE0022 [TestMethod] - [Timeout(5000)] + [Timeout(5000, CooperativeCancellation = true)] #pragma warning disable IDE0022 public void NorthSouthIf_NonZero_GoesNorth() { @@ -124,7 +124,7 @@ public void NorthSouthIf_NonZero_GoesNorth() #pragma warning restore IDE0022 [TestMethod] - [Timeout(5000)] + [Timeout(5000, CooperativeCancellation = true)] #pragma warning disable IDE0022 public void EastWestIf_NonZero_GoesWest() { @@ -171,8 +171,9 @@ public void Trampoline_SkipsOne() #pragma warning restore IDE0022 [TestMethod] + [Timeout(5000, CooperativeCancellation = true)] public void SgmlSpaces_DoNotReflect() - => Assert.AreEqual("1 ", Run("1\t\f\v.@")); + => Assert.AreEqual("1 ", Run("1\t\v.@")); // ── FungeSpace get/put ──────────────────────────────────────────────── @@ -180,15 +181,35 @@ public void SgmlSpaces_DoNotReflect() #pragma warning disable IDE0022 public void GetPut_ReadWrite() { - // p pops y,x,v. Build v=65 via 8*8+1, then store at (5,0) and read back. - Assert.AreEqual("65 ", Run("88*1+50p50g.@")); + // p pops z,y,x,v. Build v=65 via 8*8+1, then store at (5,0,0) and read back. + Assert.AreEqual("65 ", Run("88*1+500p500g.@")); } #pragma warning restore IDE0022 + [TestMethod] + [Timeout(5000, CooperativeCancellation = true)] + public void GoHigh_ChangesDeltaToNegativeZ() + => Assert.AreEqual(7, RunGetExitCode("h\f\f>7q")); + + [TestMethod] + [Timeout(5000, CooperativeCancellation = true)] + public void GoLow_ChangesDeltaToPositiveZ() + => Assert.AreEqual(7, RunGetExitCode("l\f>7q")); + + [TestMethod] + [Timeout(5000, CooperativeCancellation = true)] + public void HighLowIf_Zero_GoesLow() + => Assert.AreEqual(1, RunGetExitCode("0m\f >1q\f >2q")); + + [TestMethod] + [Timeout(5000, CooperativeCancellation = true)] + public void HighLowIf_NonZero_GoesHigh() + => Assert.AreEqual(2, RunGetExitCode("1m\f >1q\f >2q")); + // ── Hello World ─────────────────────────────────────────────────────── [TestMethod] - [Timeout(5000)] + [Timeout(5000, CooperativeCancellation = true)] #pragma warning disable IDE0022 public void HelloWorld_Classic() { @@ -199,7 +220,7 @@ public void HelloWorld_Classic() #pragma warning restore IDE0022 [TestMethod] - [Timeout(5000)] + [Timeout(5000, CooperativeCancellation = true)] #pragma warning disable IDE0022 public void HelloWorld_WithExclamation() { diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index 1dd6a90..86f1a18 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -8,7 +8,7 @@ namespace Esolang.Funge.Processor; /// Supports the full core instruction set including concurrent IPs (t) and /// the stack stack ({/}/u). /// Fingerprints ((/)) and file I/O (i/o) reflect (not implemented). -/// 3-D instructions (h/l/m) reflect in 2-D mode. +/// Includes Trefunge 3-D direction instructions (h/l/m). /// public sealed partial class FungeProcessor : ITextProcessor { @@ -233,15 +233,29 @@ private void ExecuteInstruction( case 'v': ip.Delta = FungeVector.South; break; case '?': // Go Away: random cardinal direction - ip.Delta = _random.Next(4) switch + ip.Delta = _random.Next(6) switch { 0 => FungeVector.East, 1 => FungeVector.West, 2 => FungeVector.North, - _ => FungeVector.South, + 3 => FungeVector.South, + 4 => FungeVector.High, + _ => FungeVector.Low, }; break; + case 'h': // Go High (3-D) + ip.Delta = FungeVector.High; + break; + + case 'l': // Go Low (3-D) + ip.Delta = FungeVector.Low; + break; + + case 'm': // High-Low If (3-D) + ip.Delta = ip.StackStack.Pop() == 0 ? FungeVector.Low : FungeVector.High; + break; + case '_': // East-West If ip.Delta = ip.StackStack.Pop() == 0 ? FungeVector.East : FungeVector.West; break; @@ -264,8 +278,8 @@ private void ExecuteInstruction( case 'x': // Absolute Delta { - int dy = ip.StackStack.Pop(), dx = ip.StackStack.Pop(); - ip.Delta = new FungeVector(dx, dy); + int dz = ip.StackStack.Pop(), dy = ip.StackStack.Pop(), dx = ip.StackStack.Pop(); + ip.Delta = new FungeVector(dx, dy, dz); break; } @@ -319,18 +333,18 @@ private void ExecuteInstruction( break; // ── FungeSpace get/put ─────────────────────────────────────────── - case 'g': // Get: read cell at (x+offset, y+offset) + case 'g': // Get: read cell at (x+offset, y+offset, z+offset) { - int y = ip.StackStack.Pop(), x = ip.StackStack.Pop(); - ip.StackStack.Push(_space[new FungeVector(x + ip.Offset.X, y + ip.Offset.Y)]); + int z = ip.StackStack.Pop(), y = ip.StackStack.Pop(), x = ip.StackStack.Pop(); + ip.StackStack.Push(_space[new FungeVector(x + ip.Offset.X, y + ip.Offset.Y, z + ip.Offset.Z)]); break; } - case 'p': // Put: write cell at (x+offset, y+offset) + case 'p': // Put: write cell at (x+offset, y+offset, z+offset) { - int y = ip.StackStack.Pop(), x = ip.StackStack.Pop(); + int z = ip.StackStack.Pop(), y = ip.StackStack.Pop(), x = ip.StackStack.Pop(); var val = ip.StackStack.Pop(); - _space[new FungeVector(x + ip.Offset.X, y + ip.Offset.Y)] = val; + _space[new FungeVector(x + ip.Offset.X, y + ip.Offset.Y, z + ip.Offset.Z)] = val; break; } @@ -440,6 +454,7 @@ private void ExecuteInstruction( // Push storage offset to current TOSS (will become SOSS) ip.StackStack.Push(ip.Offset.X); ip.StackStack.Push(ip.Offset.Y); + ip.StackStack.Push(ip.Offset.Z); // Push new empty stack (old TOSS becomes SOSS) ip.StackStack.PushNewStack(); @@ -478,10 +493,11 @@ private void ExecuteInstruction( // Pop current TOSS (discard remaining items) ip.StackStack.PopCurrentStack(); - // Restore storage offset (Y on top, then X) + // Restore storage offset (Z on top, then Y, then X) + var oz = ip.StackStack.Pop(); var oy = ip.StackStack.Pop(); var ox = ip.StackStack.Pop(); - ip.Offset = new FungeVector(ox, oy); + ip.Offset = new FungeVector(ox, oy, oz); // If n < 0, discard |n| items from (now current) TOSS if (n < 0) @@ -534,7 +550,7 @@ private void ExecuteInstruction( break; } - // ── Optional / 3-D-only (reflect) ──────────────────────────────── + // ── Optional (reflect) ──────────────────────────────────────────── case '=': // Execute (system exec) – reflect { // Consume 0gnirts command string from stack @@ -545,9 +561,6 @@ private void ExecuteInstruction( case 'i': // Input File – reflect case 'o': // Output File – reflect - case 'h': // Go High (3-D) – reflect - case 'l': // Go Low (3-D) – reflect - case 'm': // High-Low If (3-D) – reflect ip.Delta = ip.Delta.Reflect(); break; @@ -582,27 +595,32 @@ private void PushSysInfo(InstructionPointer ip, int _, int c) items.Add(0); // 6. Path separator items.Add(Path.DirectorySeparatorChar); - // 7. Number of dimensions (2 = Befunge) - items.Add(2); + // 7. Number of dimensions (3 = Trefunge) + items.Add(3); // 8. IP unique ID items.Add(ip.Id); // 9. IP team number items.Add(0); - // 10-11. IP position (X, Y; Y on top) + // 10-12. IP position (X, Y, Z; Z on top) items.Add(ip.Position.X); items.Add(ip.Position.Y); - // 12-13. IP delta (dX, dY; dY on top) + items.Add(ip.Position.Z); + // 13-15. IP delta (dX, dY, dZ; dZ on top) items.Add(ip.Delta.X); items.Add(ip.Delta.Y); - // 14-15. Storage offset (oX, oY; oY on top) + items.Add(ip.Delta.Z); + // 16-18. Storage offset (oX, oY, oZ; oZ on top) items.Add(ip.Offset.X); items.Add(ip.Offset.Y); - // 16-17. Least point of LSAB (minX, minY; minY on top) + items.Add(ip.Offset.Z); + // 19-21. Least point of LSAB (minX, minY, minZ; minZ on top) items.Add(_space.MinX); items.Add(_space.MinY); - // 18-19. Greatest point of LSAB (maxX, maxY; maxY on top) + items.Add(_space.MinZ); + // 22-24. Greatest point of LSAB (maxX, maxY, maxZ; maxZ on top) items.Add(_space.MaxX); items.Add(_space.MaxY); + items.Add(_space.MaxZ); var now = DateTime.Now; // 20. Current date: (year-1900)*10000 + month*100 + day diff --git a/Processor/README.md b/Processor/README.md index d9ca457..26b5b83 100644 --- a/Processor/README.md +++ b/Processor/README.md @@ -14,7 +14,7 @@ It implements the full Funge-98 core instruction set including concurrent Instru | Stack | `0`–`9` `a`–`f` (push), `:` (dup), `$` (pop), `\` (swap), `n` (clear) | | Arithmetic | `+` `-` `*` `/` `%` | | Comparison | `` ` `` `!` | -| Direction | `>` `<` `^` `v` `?` `[` `]` `r` `x` | +| Direction | `>` `<` `^` `v` `h` `l` `?` `[` `]` `r` `x` `m` | | Movement | `#` (trampoline), `;` (jump over), `j` (jump forward) | | String mode | `"` | | Branching | `_` `|` `w` | @@ -24,11 +24,11 @@ It implements the full Funge-98 core instruction set including concurrent Instru | Concurrency | `t` (split IP) | | Stack stack | `{` `}` `u` | | Misc | `z` (no-op), `q` (quit) | -| Reflected | `(` `)` `i` `o` `h` `l` `m` (fingerprints / 3-D / file I/O not implemented) | +| Reflected | `(` `)` `i` `o` (fingerprints / file I/O not implemented) | ## Funge-98 Compliance -Targets **Befunge-98** (2D). Trefunge-98 and fingerprint extensions are intentionally out of scope. +Targets **Funge-98** with 3D navigation (`h`/`l`/`m`). Fingerprint extensions are intentionally out of scope. | Category | Instructions | Status | |---|---|---| @@ -50,7 +50,7 @@ Targets **Befunge-98** (2D). Trefunge-98 and fingerprint extensions are intentio | File I/O | `i` `o` | ❌ reflects (not implemented) | | System exec | `=` | ❌ reflects (not implemented) | | Fingerprints | `(` `)` `A`–`Z` | ❌ reflects (not implemented) | -| 3D (Trefunge) | `h` `l` `m` | ❌ reflects (2D only) | +| 3D (Trefunge) | `h` `l` `m` | ✅ | ## Installation diff --git a/README.md b/README.md index 336c05a..3e73add 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,26 @@ For runnable examples covering all return types and inline source, see: - [UseConsole sample](./samples/Generator.UseConsole/README.md) +## Funge-98 Support Status + +Current implementation status across packages: + +| Area | Status | +|---|---| +| Core Funge-98 instructions | ✅ Implemented | +| Trefunge 3D navigation (`h` `l` `m`) | ✅ Implemented | +| Coordinates and storage space | ✅ 3D (`X`,`Y`,`Z`) | +| Fingerprints (`(` `)` / `A`-`Z`) | ❌ Not implemented (reflect) | +| File I/O (`i` `o`) | ❌ Not implemented (reflect) | +| System exec (`=`) | ❌ Not implemented (reflect) | + +Details: + +- Parser behavior and space model: [Parser README](./Parser/README.md) +- Runtime execution and instruction compliance: [Processor README](./Processor/README.md) +- Generated runtime subset and limitations: [Generator README](./Generator/README.md) +- CLI behavior and scope: [Interpreter README](./Interpreter/README.md) + ## Install ```bash diff --git a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs index 0289113..1b87058 100644 --- a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs +++ b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs @@ -21,6 +21,9 @@ // Inline source — same "Hello, World!" program embedded as a string literal Console.WriteLine($"{nameof(FungeSample.HelloWorldInline)}: {FungeSample.HelloWorldInline()}"); +// 3D (Trefunge) — layer Z=0 uses 'l' to jump into layer Z=1 where Hello World runs +Console.WriteLine($"{nameof(FungeSample.HelloWorld3D)}: {FungeSample.HelloWorld3D()}"); + static async Task ToByteArrayAsync(IAsyncEnumerable source) { var list = new List(); @@ -51,5 +54,9 @@ partial class FungeSample // InlineSource: no .b98 file needed — Funge-98 code is embedded directly [GenerateFungeMethod(InlineSource = "64+\"!dlroW ,olleH\">:#,_@")] public static partial string HelloWorldInline(); + + // 3D (Trefunge): layer Z=0 executes 'l' (go low) to enter layer Z=1 where Hello World runs + [GenerateFungeMethod("Programs/hello3d.b98")] + public static partial string HelloWorld3D(); } } diff --git a/samples/Generator.UseConsole/Programs/hello3d.b98 b/samples/Generator.UseConsole/Programs/hello3d.b98 new file mode 100644 index 0000000..70a552f --- /dev/null +++ b/samples/Generator.UseConsole/Programs/hello3d.b98 @@ -0,0 +1 @@ +l >64+"!dlroW ,olleH">:#,_@ \ No newline at end of file diff --git a/samples/Generator.UseConsole/README.md b/samples/Generator.UseConsole/README.md index 37099dc..a2f70f4 100644 --- a/samples/Generator.UseConsole/README.md +++ b/samples/Generator.UseConsole/README.md @@ -9,7 +9,8 @@ ``` samples/Generator.UseConsole/ ├── Programs/ -│ └── hello.b98 # Funge-98 ソースファイル +│ ├── hello.b98 # 2D Hello World +│ └── hello3d.b98 # 3D (Trefunge) Hello World ├── Esolang.Funge.Generator.UseConsole.cs # サンプルコード(top-level statements) └── Esolang.Funge.Generator.UseConsole.csproj ``` @@ -79,6 +80,7 @@ namespace Esolang.Funge | `HelloWorldBytes` | `partial IEnumerable HelloWorldBytes()` | 出力バイトを同期で列挙する | | `HelloWorldBytesAsync` | `partial IAsyncEnumerable HelloWorldBytesAsync()` | 出力バイトを非同期で列挙する | | `HelloWorldInline` | `partial string HelloWorldInline()` | インラインソース(後述) | +| `HelloWorld3D` | `partial string HelloWorld3D()` | 3Dソース(`\f`でZレイヤー分割) | ## インラインソース @@ -111,4 +113,5 @@ HelloWorldWriter: Hello, World! HelloWorldBytes: Hello, World! HelloWorldBytesAsync: Hello, World! HelloWorldInline: Hello, World! +HelloWorld3D: Hello, World! ``` From f823cb31300a73db3e12b07a87669df002e15789 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 00:15:28 +0900 Subject: [PATCH 06/17] feat: implement Funge-98 file I/O (i/o) Add processor and generated runtime support for i/o instructions, update sysinfo flags, add processor/generator tests, and refresh docs/changelog support matrices. --- CHANGELOG.md | 2 + Generator.Tests/FungeMethodGeneratorTests.cs | 89 +++++++++ Generator/MethodGenerator.Runtime.cs | 162 +++++++++++++++ Generator/README.md | 2 +- Interpreter/README.md | 2 +- Processor.Tests/FungeProcessorTests.cs | 51 +++++ Processor/FungeProcessor.cs | 199 ++++++++++++++++++- Processor/README.md | 4 +- README.md | 2 +- 9 files changed, 500 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c129729..9849dfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,9 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Generator`: added return-type support for `int`, `Task`, and `ValueTask`, and aligned `q` handling to return the popped exit code (`@` returns `0`). - `Esolang.Funge.Parser`: parser and space model now support 3D coordinates (`X`, `Y`, `Z`), with form-feed (`\f`) treated as a Z-layer separator. - `Esolang.Funge.Processor`: enabled Trefunge 3D navigation instructions `h` / `l` / `m`, extended `?` to 6 directions, and upgraded `g` / `p` / `x` to 3D operands. +- `Esolang.Funge.Processor`: implemented filesystem instructions `i` / `o` (file input/output). - `Esolang.Funge.Generator`: generated runtime now uses 3D execution space (XYZ bounds/cells), supports `h` / `l` / `m`, and handles 3D `g` / `p` / `x` semantics. +- `Esolang.Funge.Generator`: generated runtime now supports filesystem instructions `i` / `o`. - Docs: updated package README compliance tables and added 3D notes/examples (including `\f` layer separator guidance). ## [1.0.1] - 2026-05-07 diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index 99685b9..2e23464 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -661,6 +661,95 @@ await Task.Factory.StartNew(() => }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } + [TestMethod] + public async Task Runtime_FileInput_LoadsIntoSpace() + { + var originalDir = Directory.GetCurrentDirectory(); + var tempDir = Path.Combine(Path.GetTempPath(), $"funge-gen-io-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + + try + { + Directory.SetCurrentDirectory(tempDir); + File.WriteAllText("input.txt", "A"); + + var reversed = new string("input.txt".Reverse().ToArray()); + var program = $"00000\"{reversed}\"in000gq"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("file-in.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("file-in.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (int?)m.Invoke(null, []); + Assert.AreEqual(65, result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + Directory.Delete(tempDir, recursive: true); + } + } + + [TestMethod] + public async Task Runtime_FileOutput_WritesRegion() + { + var originalDir = Directory.GetCurrentDirectory(); + var tempDir = Path.Combine(Path.GetTempPath(), $"funge-gen-io-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + + try + { + Directory.SetCurrentDirectory(tempDir); + + var reversed = new string("output.txt".Reverse().ToArray()); + var program = $"88*1+000p00000000\"{reversed}\"o@"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("file-out.b98")] + public static partial void Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("file-out.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + _ = m.Invoke(null, []); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + + var bytes = File.ReadAllBytes(Path.Combine(tempDir, "output.txt")); + CollectionAssert.AreEqual(new byte[] { 65 }, bytes); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + Directory.Delete(tempDir, recursive: true); + } + } + [TestMethod] public void Diagnostic_SourceFileNotFound_FG0004() { diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index d760122..21fd5d3 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -47,6 +47,146 @@ void SetCell(int x, int y, int z, int val) int Pop() { if (stack.Count == 0) return 0; var v = stack[stack.Count - 1]; stack.RemoveAt(stack.Count - 1); return v; } void Push(int v) => stack.Add(v); + (int x, int y, int z) PopVector() + { + int z = Pop(), y = Pop(), x = Pop(); + return (x, y, z); + } + + void PushVector(int x, int y, int z) + { + Push(x); + Push(y); + Push(z); + } + + bool TryPopZeroTerminatedString(out string result) + { + var chars = new List(); + while (true) + { + int value = Pop(); + if (value == 0) + { + result = new string(chars.ToArray()); + return true; + } + + if (value < char.MinValue || value > char.MaxValue) + { + result = string.Empty; + return false; + } + + chars.Add((char)value); + } + } + + bool TryInputFile(int baseX, int baseY, int baseZ, string fileName, bool binaryMode, out (int x, int y, int z) size) + { + size = (0, 0, 0); + + byte[] bytes; + try + { + bytes = File.ReadAllBytes(fileName); + } + catch + { + return false; + } + + int x = 0, y = 0, z = 0; + int maxX = 0, maxY = 0, maxZ = 0; + bool wroteAny = false; + + foreach (byte raw in bytes) + { + int cell = raw; + + if (!binaryMode) + { + if (cell == '\r') + continue; + if (cell == '\n') + { + x = 0; + y++; + continue; + } + if (cell == '\f') + { + x = 0; + y = 0; + z++; + continue; + } + if (cell is '\t' or '\v') + cell = ' '; + } + + if (binaryMode || cell != ' ') + SetCell(baseX + x, baseY + y, baseZ + z, cell); + + wroteAny = true; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + if (z > maxZ) maxZ = z; + x++; + } + + size = wroteAny ? (maxX, maxY, maxZ) : (0, 0, 0); + return true; + } + + bool TryOutputFile(int baseX, int baseY, int baseZ, int sx, int sy, int sz, string fileName, bool linearText) + { + sx = Math.Max(0, sx); + sy = Math.Max(0, sy); + sz = Math.Max(0, sz); + + var rows = new List(); + for (int z = 0; z <= sz; z++) + { + for (int y = 0; y <= sy; y++) + { + var chars = new char[sx + 1]; + for (int x = 0; x <= sx; x++) + { + int c = GetCell(baseX + x, baseY + y, baseZ + z); + chars[x] = c is >= char.MinValue and <= char.MaxValue ? (char)c : ' '; + } + + var row = new string(chars); + rows.Add(linearText ? row.TrimEnd(' ') : row); + } + + if (z != sz) + rows.Add("\f"); + } + + if (linearText) + { + while (rows.Count > 0 && rows[rows.Count - 1].Length == 0) + rows.RemoveAt(rows.Count - 1); + } + + var text = string.Join("\n", rows); + var bytes = new byte[text.Length]; + for (int i = 0; i < text.Length; i++) + bytes[i] = (byte)(text[i] & 0xFF); + + try + { + File.WriteAllBytes(fileName, bytes); + return true; + } + catch + { + return false; + } + } + (int nx, int ny, int nz) Advance(int x, int y, int z, int ddx, int ddy, int ddz) { if (maxX < minX) return (x, y, z); @@ -142,6 +282,28 @@ void ExecuteInstruction(int cell, ref bool suppressAdvance) if (!hasInput) throw new InvalidOperationException("Funge input instruction '~' executed without an input interface."); int ch = input.Read(); if(ch<0){dx=-dx;dy=-dy;dz=-dz;}else Push(ch); break; } + case 'i': + { + if (!TryPopZeroTerminatedString(out string fileName)) { dx = -dx; dy = -dy; dz = -dz; break; } + int flags = Pop(); + var (vx, vy, vz) = PopVector(); + int baseX = vx, baseY = vy, baseZ = vz; + bool binaryMode = (flags & 1) != 0; + if (!TryInputFile(baseX, baseY, baseZ, fileName, binaryMode, out var vb)) { dx = -dx; dy = -dy; dz = -dz; break; } + PushVector(vx, vy, vz); + PushVector(vb.x, vb.y, vb.z); + break; + } + case 'o': + { + if (!TryPopZeroTerminatedString(out string fileName)) { dx = -dx; dy = -dy; dz = -dz; break; } + int flags = Pop(); + var (sbx, sby, sbz) = PopVector(); + var (vax, vay, vaz) = PopVector(); + bool linearText = (flags & 1) != 0; + if (!TryOutputFile(vax, vay, vaz, sbx, sby, sbz, fileName, linearText)) { dx = -dx; dy = -dy; dz = -dz; } + break; + } case 'k': { int n = Pop(); diff --git a/Generator/README.md b/Generator/README.md index b9db944..5785881 100644 --- a/Generator/README.md +++ b/Generator/README.md @@ -150,7 +150,7 @@ including Trefunge 3D navigation (`h` / `l` / `m`) but excluding concurrency, st | Concurrency | `t` | ❌ not implemented (single IP only) | | Stack stack | `{` `}` `u` | ❌ not implemented | | System info | `y` | ❌ not implemented | -| File I/O | `i` `o` | ❌ not implemented | +| File I/O | `i` `o` | ✅ | | System exec | `=` | ❌ not implemented | | Fingerprints | `(` `)` `A`–`Z` | ❌ reflects (not implemented) | | 3D (Trefunge) | `h` `l` `m` | ✅ | diff --git a/Interpreter/README.md b/Interpreter/README.md index ba95c71..0e87dfb 100644 --- a/Interpreter/README.md +++ b/Interpreter/README.md @@ -41,7 +41,7 @@ For detailed processor-level behavior, refer to the processor package documentat | Standard I/O (`&` `~` `,` `.`) connected to stdin / stdout | ✅ | | Exit code via `q` | ✅ | | Fingerprints (`(` `)` `A`–`Z`) | ❌ reflects (not implemented) | -| File I/O (`i` `o`) | ❌ reflects (not implemented) | +| File I/O (`i` `o`) | ✅ | | System exec (`=`) | ❌ reflects (not implemented) | | 3D / Trefunge (`h` `l` `m`) | ✅ | diff --git a/Processor.Tests/FungeProcessorTests.cs b/Processor.Tests/FungeProcessorTests.cs index 7e7eaa4..4914769 100644 --- a/Processor.Tests/FungeProcessorTests.cs +++ b/Processor.Tests/FungeProcessorTests.cs @@ -239,6 +239,57 @@ public void InputChar_EchoBack() public void InputInt_EchoBack() => Assert.AreEqual("42 ", Run("&.@", "42\n")); + [TestMethod] + public void InputFile_LoadsFileIntoSpace() + { + var originalDir = Directory.GetCurrentDirectory(); + var tempDir = Path.Combine(Path.GetTempPath(), $"funge-io-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + + try + { + Directory.SetCurrentDirectory(tempDir); + File.WriteAllText("input.txt", "A"); + + // Va=(0,0,0), flags=0 (text mode), STR=0"input.txt" (0gnirts) + var output = Run("00000\"txt.tupni\"in000g.@"); + Assert.AreEqual("65 ", output); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + Directory.Delete(tempDir, recursive: true); + } + } + + [TestMethod] + public void OutputFile_WritesSpaceRegion() + { + var originalDir = Directory.GetCurrentDirectory(); + var tempDir = Path.Combine(Path.GetTempPath(), $"funge-io-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + + try + { + Directory.SetCurrentDirectory(tempDir); + + // store 'A' at (0,0,0), then output Va=(0,0,0), Vb=(0,0,0), flags=0, STR=0"output.txt" + _ = Run("88*1+000p00000000\"txt.tuptuo\"o@"); + + var bytes = File.ReadAllBytes(Path.Combine(tempDir, "output.txt")); + CollectionAssert.AreEqual(new byte[] { 65 }, bytes); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + Directory.Delete(tempDir, recursive: true); + } + } + + [TestMethod] + public void SysInfo_ReportsFileIoSupportFlags() + => Assert.AreEqual("7 ", Run("1y.@")); + // ── Quit exit code ──────────────────────────────────────────────────── [TestMethod] diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index 86f1a18..7077052 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -7,7 +7,7 @@ namespace Esolang.Funge.Processor; /// Executes a Funge-98 program loaded into a . /// Supports the full core instruction set including concurrent IPs (t) and /// the stack stack ({/}/u). -/// Fingerprints ((/)) and file I/O (i/o) reflect (not implemented). +/// Fingerprints ((/)) reflect (not implemented). /// Includes Trefunge 3-D direction instructions (h/l/m). /// public sealed partial class FungeProcessor : ITextProcessor @@ -374,6 +374,47 @@ private void ExecuteInstruction( break; } + case 'i': // Input File + { + if (!TryPopZeroTerminatedString(ip.StackStack, out var fileName)) + { + ip.Delta = ip.Delta.Reflect(); + break; + } + + var flags = ip.StackStack.Pop(); + var va = PopVector(ip.StackStack) + ip.Offset; + var binaryMode = (flags & 1) != 0; + + if (!TryInputFile(va, fileName, binaryMode, out var vb)) + { + ip.Delta = ip.Delta.Reflect(); + break; + } + + PushVector(ip.StackStack, va - ip.Offset); + PushVector(ip.StackStack, vb); + break; + } + + case 'o': // Output File + { + if (!TryPopZeroTerminatedString(ip.StackStack, out var fileName)) + { + ip.Delta = ip.Delta.Reflect(); + break; + } + + var flags = ip.StackStack.Pop(); + var vb = PopVector(ip.StackStack); + var va = PopVector(ip.StackStack) + ip.Offset; + var linearText = (flags & 1) != 0; + + if (!TryOutputFile(va, vb, fileName, linearText)) + ip.Delta = ip.Delta.Reflect(); + break; + } + // ── Control flow ───────────────────────────────────────────────── case '@': // Stop this IP ip.IsStopped = true; @@ -559,11 +600,6 @@ private void ExecuteInstruction( break; } - case 'i': // Input File – reflect - case 'o': // Output File – reflect - ip.Delta = ip.Delta.Reflect(); - break; - default: // A-Z: fingerprint-defined; reflect if not loaded if (cell is >= 'A' and <= 'Z') @@ -573,6 +609,153 @@ private void ExecuteInstruction( } } + private static FungeVector PopVector(StackStack stack) + { + var z = stack.Pop(); + var y = stack.Pop(); + var x = stack.Pop(); + return new FungeVector(x, y, z); + } + + private 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) + { + var chars = new List(); + while (true) + { + var value = stack.Pop(); + if (value == 0) + { + result = new string([.. chars]); + return true; + } + + if (value is < char.MinValue or > char.MaxValue) + { + result = string.Empty; + return false; + } + + chars.Add((char)value); + } + } + + private bool TryInputFile(FungeVector leastPoint, string fileName, bool binaryMode, out FungeVector size) + { + size = new FungeVector(0, 0, 0); + + byte[] bytes; + try + { + bytes = File.ReadAllBytes(fileName); + } + catch + { + return false; + } + + var x = 0; + var y = 0; + var z = 0; + var wroteAny = false; + var maxX = 0; + var maxY = 0; + var maxZ = 0; + + foreach (var raw in bytes) + { + var cell = (int)raw; + + if (!binaryMode) + { + if (cell == '\r') + continue; + if (cell == '\n') + { + x = 0; + y++; + continue; + } + if (cell == '\f') + { + x = 0; + y = 0; + z++; + continue; + } + if (cell is '\t' or '\v') + cell = ' '; + } + + var pos = new FungeVector(leastPoint.X + x, leastPoint.Y + y, leastPoint.Z + z); + _space.EnsureBounds(pos); + + if (binaryMode || cell != ' ') + _space[pos] = cell; + + wroteAny = true; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + if (z > maxZ) maxZ = z; + x++; + } + + size = wroteAny ? new FungeVector(maxX, maxY, maxZ) : new FungeVector(0, 0, 0); + return true; + } + + private bool TryOutputFile(FungeVector leastPoint, FungeVector size, string fileName, bool linearText) + { + var sx = Math.Max(0, size.X); + var sy = Math.Max(0, size.Y); + var sz = Math.Max(0, size.Z); + + var rows = new List(); + for (var z = 0; z <= sz; z++) + { + for (var y = 0; y <= sy; y++) + { + var chars = new char[sx + 1]; + for (var x = 0; x <= sx; x++) + { + var c = _space[new FungeVector(leastPoint.X + x, leastPoint.Y + y, leastPoint.Z + z)]; + chars[x] = c is >= char.MinValue and <= char.MaxValue ? (char)c : ' '; + } + + var row = new string(chars); + rows.Add(linearText ? row.TrimEnd(' ') : row); + } + + if (z != sz) + rows.Add("\f"); + } + + if (linearText) + { + while (rows.Count > 0 && rows[^1].Length == 0) + rows.RemoveAt(rows.Count - 1); + } + + var text = string.Join("\n", rows); + var bytes = text.Select(static ch => (byte)(ch & 0xFF)).ToArray(); + + try + { + File.WriteAllBytes(fileName, bytes); + return true; + } + catch + { + return false; + } + } + /// /// Pushes system information onto the TOSS of the given IP. /// If is greater than zero, only item @@ -583,8 +766,8 @@ private 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 = []; - // 1. Flags: bit 0 = /t (concurrency supported) - items.Add(1); + // 1. Flags: /t + /i + /o supported + items.Add(0x01 | 0x02 | 0x04); // 2. Cell size in bytes items.Add(4); // 3. Interpreter handprint ("Fung" as big-endian int) diff --git a/Processor/README.md b/Processor/README.md index 26b5b83..896c625 100644 --- a/Processor/README.md +++ b/Processor/README.md @@ -24,7 +24,7 @@ It implements the full Funge-98 core instruction set including concurrent Instru | Concurrency | `t` (split IP) | | Stack stack | `{` `}` `u` | | Misc | `z` (no-op), `q` (quit) | -| Reflected | `(` `)` `i` `o` (fingerprints / file I/O not implemented) | +| Reflected | `(` `)` (fingerprints not implemented) | ## Funge-98 Compliance @@ -47,7 +47,7 @@ Targets **Funge-98** with 3D navigation (`h`/`l`/`m`). Fingerprint extensions ar | Stack stack | `{` `}` `u` | ✅ | | System info | `y` | 🟡 env vars / command-line args are empty | | Misc | `z` `@` `q` | ✅ | -| File I/O | `i` `o` | ❌ reflects (not implemented) | +| File I/O | `i` `o` | ✅ | | System exec | `=` | ❌ reflects (not implemented) | | Fingerprints | `(` `)` `A`–`Z` | ❌ reflects (not implemented) | | 3D (Trefunge) | `h` `l` `m` | ✅ | diff --git a/README.md b/README.md index 3e73add..3ccbecf 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Current implementation status across packages: | Trefunge 3D navigation (`h` `l` `m`) | ✅ Implemented | | Coordinates and storage space | ✅ 3D (`X`,`Y`,`Z`) | | Fingerprints (`(` `)` / `A`-`Z`) | ❌ Not implemented (reflect) | -| File I/O (`i` `o`) | ❌ Not implemented (reflect) | +| File I/O (`i` `o`) | ✅ Implemented | | System exec (`=`) | ❌ Not implemented (reflect) | Details: From 031bb44820ccc2c3b231a0bf73be49f94d1f0c34 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 00:40:53 +0900 Subject: [PATCH 07/17] feat: implement Funge-98 system exec (=) Add processor and generated runtime support for '=' with exit-code push semantics, update sysinfo flags/paradigm, add processor/generator tests, and refresh docs/changelog support tables. --- CHANGELOG.md | 2 + Generator.Tests/FungeMethodGeneratorTests.cs | 60 ++++++++++++++++++++ Generator/MethodGenerator.Runtime.cs | 51 +++++++++++++++++ Generator/README.md | 2 +- Interpreter/README.md | 2 +- Processor.Tests/FungeProcessorTests.cs | 21 ++++++- Processor/FungeProcessor.cs | 59 ++++++++++++++++--- Processor/README.md | 2 +- README.md | 2 +- 9 files changed, 188 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9849dfb..e11d224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,8 +22,10 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Parser`: parser and space model now support 3D coordinates (`X`, `Y`, `Z`), with form-feed (`\f`) treated as a Z-layer separator. - `Esolang.Funge.Processor`: enabled Trefunge 3D navigation instructions `h` / `l` / `m`, extended `?` to 6 directions, and upgraded `g` / `p` / `x` to 3D operands. - `Esolang.Funge.Processor`: implemented filesystem instructions `i` / `o` (file input/output). +- `Esolang.Funge.Processor`: implemented system execution instruction `=` (returns process exit code on stack). - `Esolang.Funge.Generator`: generated runtime now uses 3D execution space (XYZ bounds/cells), supports `h` / `l` / `m`, and handles 3D `g` / `p` / `x` semantics. - `Esolang.Funge.Generator`: generated runtime now supports filesystem instructions `i` / `o`. +- `Esolang.Funge.Generator`: generated runtime now supports system execution instruction `=`. - Docs: updated package README compliance tables and added 3D notes/examples (including `\f` layer separator guidance). ## [1.0.1] - 2026-05-07 diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index 2e23464..b8e5cce 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -750,6 +750,66 @@ await Task.Factory.StartNew(() => } } + [TestMethod] + public async Task Runtime_SystemExec_ReturnsExitCode() + { + const string command = "exit 7"; + var reversed = new string(command.Reverse().ToArray()); + var program = $"0\"{reversed}\"=q"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("system-exec.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("system-exec.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (int?)m.Invoke(null, []); + Assert.AreEqual(7, result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + [TestMethod] + public async Task Runtime_SystemExec_FailureIsNonZero() + { + const string command = "this_command_should_not_exist_12345"; + var reversed = new string(command.Reverse().ToArray()); + var program = $"0\"{reversed}\"=q"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("system-exec-fail.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("system-exec-fail.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (int?)m.Invoke(null, []); + Assert.AreNotEqual(0, result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + [TestMethod] public void Diagnostic_SourceFileNotFound_FG0004() { diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index 21fd5d3..6ed5232 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -18,6 +18,7 @@ static string BuildRuntimeSource() => """ #pragma warning disable CS1591 using System; using System.Collections.Generic; + using System.Diagnostics; using System.IO; namespace Esolang.Funge.__Generated @@ -187,6 +188,50 @@ bool TryOutputFile(int baseX, int baseY, int baseZ, int sx, int sy, int sz, stri } } + int ExecuteSystemCommand(string command) + { + try + { + bool isWindows = + Environment.OSVersion.Platform == PlatformID.Win32NT || + Environment.OSVersion.Platform == PlatformID.Win32S || + Environment.OSVersion.Platform == PlatformID.Win32Windows || + Environment.OSVersion.Platform == PlatformID.WinCE; + + string fileName; + string arguments; + if (isWindows) + { + fileName = "cmd.exe"; + arguments = "/c " + command; + } + else + { + fileName = "/bin/sh"; + arguments = "-c \"" + command.Replace("\"", "\\\"") + "\""; + } + + var processStartInfo = new ProcessStartInfo(fileName, arguments) + { + UseShellExecute = false, + RedirectStandardOutput = false, + RedirectStandardError = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(processStartInfo); + if (process is null) + return -1; + + process.WaitForExit(); + return process.ExitCode; + } + catch + { + return -1; + } + } + (int nx, int ny, int nz) Advance(int x, int y, int z, int ddx, int ddy, int ddz) { if (maxX < minX) return (x, y, z); @@ -304,6 +349,12 @@ void ExecuteInstruction(int cell, ref bool suppressAdvance) if (!TryOutputFile(vax, vay, vaz, sbx, sby, sbz, fileName, linearText)) { dx = -dx; dy = -dy; dz = -dz; } break; } + case '=': + { + if (!TryPopZeroTerminatedString(out string command)) { dx = -dx; dy = -dy; dz = -dz; break; } + Push(ExecuteSystemCommand(command)); + break; + } case 'k': { int n = Pop(); diff --git a/Generator/README.md b/Generator/README.md index 5785881..6c1aae7 100644 --- a/Generator/README.md +++ b/Generator/README.md @@ -151,7 +151,7 @@ including Trefunge 3D navigation (`h` / `l` / `m`) but excluding concurrency, st | Stack stack | `{` `}` `u` | ❌ not implemented | | System info | `y` | ❌ not implemented | | File I/O | `i` `o` | ✅ | -| System exec | `=` | ❌ not implemented | +| System exec | `=` | ✅ | | Fingerprints | `(` `)` `A`–`Z` | ❌ reflects (not implemented) | | 3D (Trefunge) | `h` `l` `m` | ✅ | | ND-generalized space | dimensions > 3 | ❌ not implemented | diff --git a/Interpreter/README.md b/Interpreter/README.md index 0e87dfb..f6c9a33 100644 --- a/Interpreter/README.md +++ b/Interpreter/README.md @@ -42,7 +42,7 @@ For detailed processor-level behavior, refer to the processor package documentat | Exit code via `q` | ✅ | | Fingerprints (`(` `)` `A`–`Z`) | ❌ reflects (not implemented) | | File I/O (`i` `o`) | ✅ | -| System exec (`=`) | ❌ reflects (not implemented) | +| System exec (`=`) | ✅ | | 3D / Trefunge (`h` `l` `m`) | ✅ | ## References diff --git a/Processor.Tests/FungeProcessorTests.cs b/Processor.Tests/FungeProcessorTests.cs index 4914769..ac14038 100644 --- a/Processor.Tests/FungeProcessorTests.cs +++ b/Processor.Tests/FungeProcessorTests.cs @@ -24,6 +24,9 @@ private int RunGetExitCode(string source) return proc.Run(TestContext.CancellationTokenSource.Token); } + private static string EncodeZeroGnirts(string value) + => $"0\"{new string(value.Reverse().ToArray())}\""; + // ── Termination ──────────────────────────────────────────────────────── [TestMethod] @@ -288,7 +291,23 @@ public void OutputFile_WritesSpaceRegion() [TestMethod] public void SysInfo_ReportsFileIoSupportFlags() - => Assert.AreEqual("7 ", Run("1y.@")); + => Assert.AreEqual("15 ", Run("1y.@")); + + [TestMethod] + public void SystemExec_ReturnsExitCode() + { + const string command = "exit 7"; + var program = $"{EncodeZeroGnirts(command)}=.@"; + Assert.AreEqual("7 ", Run(program)); + } + + [TestMethod] + public void SystemExec_SetsNonZeroOnCommandFailure() + { + const string command = "this_command_should_not_exist_12345"; + var program = $"{EncodeZeroGnirts(command)}=q"; + Assert.AreNotEqual(0, RunGetExitCode(program)); + } // ── Quit exit code ──────────────────────────────────────────────────── diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index 7077052..03cfbd6 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -1,5 +1,6 @@ using Esolang.Funge.Parser; using Esolang.Processor; +using System.Diagnostics; namespace Esolang.Funge.Processor; @@ -592,11 +593,15 @@ private void ExecuteInstruction( } // ── Optional (reflect) ──────────────────────────────────────────── - case '=': // Execute (system exec) – reflect + case '=': // Execute (system exec) { - // Consume 0gnirts command string from stack - while (ip.StackStack.Pop() != 0) { } - ip.Delta = ip.Delta.Reflect(); + if (!TryPopZeroTerminatedString(ip.StackStack, out var command)) + { + ip.Delta = ip.Delta.Reflect(); + break; + } + + ip.StackStack.Push(ExecuteSystemCommand(command)); break; } @@ -756,6 +761,44 @@ private bool TryOutputFile(FungeVector leastPoint, FungeVector size, string file } } + private static int ExecuteSystemCommand(string command) + { + try + { + var processStartInfo = new ProcessStartInfo + { + UseShellExecute = false, + RedirectStandardOutput = false, + RedirectStandardError = false, + CreateNoWindow = true, + }; + + if (OperatingSystem.IsWindows()) + { + processStartInfo.FileName = "cmd.exe"; + processStartInfo.ArgumentList.Add("/c"); + processStartInfo.ArgumentList.Add(command); + } + else + { + processStartInfo.FileName = "/bin/sh"; + processStartInfo.ArgumentList.Add("-c"); + processStartInfo.ArgumentList.Add(command); + } + + using var process = Process.Start(processStartInfo); + if (process is null) + return -1; + + process.WaitForExit(); + return process.ExitCode; + } + catch + { + return -1; + } + } + /// /// Pushes system information onto the TOSS of the given IP. /// If is greater than zero, only item @@ -766,16 +809,16 @@ private 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 = []; - // 1. Flags: /t + /i + /o supported - items.Add(0x01 | 0x02 | 0x04); + // 1. Flags: /t + /i + /o + /= supported + items.Add(0x01 | 0x02 | 0x04 | 0x08); // 2. Cell size in bytes items.Add(4); // 3. Interpreter handprint ("Fung" as big-endian int) items.Add(unchecked((int)0x46756E67u)); // 4. Version (Funge-98 = 9800) items.Add(9800); - // 5. Operating paradigm (0 = system() unavailable) - items.Add(0); + // 5. Operating paradigm (1 = equivalent to C system() behavior) + items.Add(1); // 6. Path separator items.Add(Path.DirectorySeparatorChar); // 7. Number of dimensions (3 = Trefunge) diff --git a/Processor/README.md b/Processor/README.md index 896c625..e726061 100644 --- a/Processor/README.md +++ b/Processor/README.md @@ -48,7 +48,7 @@ Targets **Funge-98** with 3D navigation (`h`/`l`/`m`). Fingerprint extensions ar | System info | `y` | 🟡 env vars / command-line args are empty | | Misc | `z` `@` `q` | ✅ | | File I/O | `i` `o` | ✅ | -| System exec | `=` | ❌ reflects (not implemented) | +| System exec | `=` | ✅ | | Fingerprints | `(` `)` `A`–`Z` | ❌ reflects (not implemented) | | 3D (Trefunge) | `h` `l` `m` | ✅ | diff --git a/README.md b/README.md index 3ccbecf..923dffa 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Current implementation status across packages: | Coordinates and storage space | ✅ 3D (`X`,`Y`,`Z`) | | Fingerprints (`(` `)` / `A`-`Z`) | ❌ Not implemented (reflect) | | File I/O (`i` `o`) | ✅ Implemented | -| System exec (`=`) | ❌ Not implemented (reflect) | +| System exec (`=`) | ✅ Implemented | Details: From 042db6fb6d90c65b7ccfa0728529b603e5a862f5 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 06:19:08 +0900 Subject: [PATCH 08/17] =?UTF-8?q?markdown=20=E3=81=AE=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Generator/README.md | 18 +++++++++--------- Interpreter/README.md | 10 +++++----- samples/Generator.UseConsole/README.md | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Generator/README.md b/Generator/README.md index 6c1aae7..4050af8 100644 --- a/Generator/README.md +++ b/Generator/README.md @@ -10,7 +10,7 @@ The generator reads the Funge-98 source (from a file or inline) and emits a comp ### Supported return types | Return type | Description | -|---|---| +| --- | --- | | `void` | Run to completion; discard output | | `int` | Return program exit code (`q` pops stack top, otherwise `0`) | | `string` | Collect all output and return as a string | @@ -26,7 +26,7 @@ The generator reads the Funge-98 source (from a file or inline) and emits a comp ### Supported parameter types | Parameter type | Role | -|---|---| +| --- | --- | | `string` | Input fed to the program (`&` / `~`) | | `System.IO.TextReader` | Input reader | | `System.IO.Pipelines.PipeReader` | Input as pipe | @@ -36,7 +36,7 @@ The generator reads the Funge-98 source (from a file or inline) and emits a comp ## Installation -``` +```bash dotnet add package Esolang.Funge.Generator ``` @@ -91,14 +91,14 @@ public static partial string HelloWorldInline(); The generator fully supports 3D Funge-98 (Trefunge) programs. Within a source file, the form-feed character (`\f`, U+000C) separates Z-layers. -``` +```.b98 # Layer Z=0 — jump into layer Z=1 l ``` -*(form-feed character here separates layers)* +### (form-feed character here separates layers) -``` +```.b98 # Layer Z=1 — runs the Hello World program >64+"!dlroW ,olleH">:#,_@ ``` @@ -106,7 +106,7 @@ l **3D direction instructions:** | Instruction | Description | -|---|---| +| --- | --- | | `h` | Set delta to High `(0, 0, −1)` | | `l` | Set delta to Low `(0, 0, +1)` | | `m` | Pop value; zero → Low, non-zero → High | @@ -116,7 +116,7 @@ The generated runtime automatically handles XYZ coordinates, Z-axis wrapping, an ## Diagnostics | ID | Severity | Description | -|---|---|---| +| --- | --- | --- | | FG0001 | Error | `sourcePath` is empty and `InlineSource` is not set | | FG0002 | Error | Unsupported return type | | FG0003 | Error | Unsupported parameter type | @@ -134,7 +134,7 @@ The generated runtime (`FungeRuntime`) implements a **single-IP, single-stack su including Trefunge 3D navigation (`h` / `l` / `m`) but excluding concurrency, stack stack operations, and fingerprints. | Category | Instructions | Status | -|---|---|---| +| --- | --- | --- | | Stack | `0`–`9` `a`–`f` `:` `$` `\` `n` | ✅ | | Arithmetic | `+` `-` `*` `/` `%` | ✅ | | Comparison | `` ` `` `!` | ✅ | diff --git a/Interpreter/README.md b/Interpreter/README.md index f6c9a33..8c005d2 100644 --- a/Interpreter/README.md +++ b/Interpreter/README.md @@ -4,23 +4,23 @@ Command-line interpreter for [Funge-98](https://github.com/catseye/Funge-98/blob ## Installation -``` +```bash dotnet tool install -g dotnet-funge ``` ## Usage -``` +```bash dotnet-funge ``` | Argument | Description | -|---|---| +| --- | --- | | `` | Path to a Funge-98 source file (`.b98`) | ### Example -``` +```bash dotnet-funge hello.b98 ``` @@ -34,7 +34,7 @@ Delegates execution to `Esolang.Funge.Processor`, including Trefunge 3D directio For detailed processor-level behavior, refer to the processor package documentation. | Area | Status | -|---|---| +| --- | --- | | Core instruction set (stack, arithmetic, comparison, direction, I/O, storage, movement) | ✅ | | Funge-98 extensions (`k` iterate, `t` concurrency, `{`/`}`/`u` stack stack) | ✅ | | System info (`y`) | 🟡 env vars / command-line args are empty | diff --git a/samples/Generator.UseConsole/README.md b/samples/Generator.UseConsole/README.md index a2f70f4..f8df6a8 100644 --- a/samples/Generator.UseConsole/README.md +++ b/samples/Generator.UseConsole/README.md @@ -6,7 +6,7 @@ ## プロジェクト構成 -``` +```text samples/Generator.UseConsole/ ├── Programs/ │ ├── hello.b98 # 2D Hello World @@ -17,7 +17,7 @@ samples/Generator.UseConsole/ ### hello.b98 -``` +```.b98 64+"!dlroW ,olleH">:#,_@ ``` @@ -73,7 +73,7 @@ namespace Esolang.Funge このサンプルでは以下のすべての戻り型を示しています。 | メソッド | 宣言 | 説明 | -|---|---|---| +| --- | --- | --- | | `HelloWorld` | `partial string HelloWorld()` | 出力を文字列として返す | | `HelloWorldAsync` | `partial Task HelloWorldAsync()` | 非同期で出力を文字列として返す | | `HelloWorldWriter` | `partial void HelloWorldWriter(TextWriter output)` | `TextWriter` に出力を書き込む | @@ -100,13 +100,13 @@ public static partial string HelloWorldInline(); ## 実行 -``` +```bash dotnet run --framework net10.0 ``` 期待される出力: -``` +```text HelloWorld: Hello, World! HelloWorldAsync: Hello, World! HelloWorldWriter: Hello, World! From e7e6bfb31cc89e933537edb6c9022bd345d81b5c Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 09:18:35 +0900 Subject: [PATCH 09/17] feat: implement Funge-98 y/offset/stack-stack in runtime layers --- .gitignore | 1 + CHANGELOG.md | 5 + Generator.Tests/FungeMethodGeneratorTests.cs | 84 ++ Generator/MethodGenerator.Runtime.cs | 883 +++++++++++++++---- Generator/README.md | 12 +- Interpreter/FungeInterpreterExtensions.cs | 11 +- Interpreter/README.md | 2 +- Processor/FungeProcessor.cs | 64 +- Processor/README.md | 2 +- 9 files changed, 855 insertions(+), 209 deletions(-) diff --git a/.gitignore b/.gitignore index 0808c4a..b5112b9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ *.suo *.user *.userosscache +*.csproj.lscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) diff --git a/CHANGELOG.md b/CHANGELOG.md index e11d224..08251ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Processor.Tests`: coverage for `RunToEnd(...)` and `RunToEndAsync(...)` on `FungeProcessor`. - `Esolang.Funge.Generator.Tests`: 3D runtime coverage for `h` / `l` / `m` and 3D `g` / `p` behavior. - `samples/Generator.UseConsole/Programs/hello3d.b98`: minimal Trefunge sample using form-feed (`\f`) layer separation. +- `Esolang.Funge.Generator.Tests`: runtime coverage for storage offset-aware `g` / `p`, stack stack transfer via `u`, and `y` capability flags. ### Changed @@ -26,7 +27,11 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Generator`: generated runtime now uses 3D execution space (XYZ bounds/cells), supports `h` / `l` / `m`, and handles 3D `g` / `p` / `x` semantics. - `Esolang.Funge.Generator`: generated runtime now supports filesystem instructions `i` / `o`. - `Esolang.Funge.Generator`: generated runtime now supports system execution instruction `=`. +- `Esolang.Funge.Processor`: `y` now includes command-line arguments and environment variables, uses Funge-98 date/time encoding (base-256), reports least-stack-area bounds as relative extents, and aligns positive-`c` pick behavior with full-stack semantics. +- `dotnet-funge` (`Esolang.Funge.Interpreter`): processor construction now passes command-line arguments and environment variables for `y` system-info reporting. +- `Esolang.Funge.Generator`: generated runtime now supports concurrency (`t`), stack stack operations (`{` / `}` / `u`), `y` system-info, and applies storage offset semantics to `g` / `p` and file I/O least-point handling. - Docs: updated package README compliance tables and added 3D notes/examples (including `\f` layer separator guidance). +- Repository hygiene: ignore `*.csproj.lscache` (C# Dev Kit cache) to avoid generated noise in working trees. ## [1.0.1] - 2026-05-07 diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index b8e5cce..3034c6e 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -661,6 +661,90 @@ await Task.Factory.StartNew(() => }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } + [TestMethod] + public async Task Runtime_StorageOffset_AppliesToGetPut() + { + const string program = "0{88*1+000p000gq"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("offset-getput.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("offset-getput.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (int?)m.Invoke(null, []); + Assert.AreEqual(65, result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + [TestMethod] + public async Task Runtime_StackStack_U_TransfersFromSoss() + { + const string program = "120{4u.@"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("stack-u.b98")] + public static partial string Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("stack-u.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (string?)m.Invoke(null, []); + Assert.AreEqual("2 ", result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + [TestMethod] + public async Task Runtime_SystemInfo_FlagsIncludesConcurrentFileExec() + { + const string program = "1yq"; + + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("sysinfo-flags.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("sysinfo-flags.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp); + await Task.Factory.StartNew(() => + { + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var result = (int?)m.Invoke(null, []); + Assert.AreEqual(15, result); + }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + [TestMethod] public async Task Runtime_FileInput_LoadsIntoSpace() { diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index 6ed5232..d1e2a94 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -17,14 +17,88 @@ static string BuildRuntimeSource() => """ #nullable enable #pragma warning disable CS1591 using System; + using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; + using System.Linq; namespace Esolang.Funge.__Generated { internal static class FungeRuntime { + private sealed class RuntimeStackStack + { + private readonly LinkedList> _stacks = new LinkedList>(); + + internal RuntimeStackStack() + { + _stacks.AddFirst(new Stack()); + } + + private RuntimeStackStack(LinkedList> stacks) + { + _stacks.Clear(); + foreach (var stack in stacks) + _stacks.AddLast(stack); + } + + internal Stack TOSS { get { return _stacks.First.Value; } } + internal Stack SOSS { get { return _stacks.Count >= 2 ? _stacks.First.Next.Value : null; } } + internal bool HasSOSS { get { return _stacks.Count >= 2; } } + internal int StackCount { get { return _stacks.Count; } } + internal IEnumerable> AllStacks { get { return _stacks; } } + + internal void Push(int value) { TOSS.Push(value); } + internal int Pop() { return TOSS.Count > 0 ? TOSS.Pop() : 0; } + internal void ClearToss() { TOSS.Clear(); } + internal void PushNewStack() { _stacks.AddFirst(new Stack()); } + + internal void PopCurrentStack() + { + if (_stacks.Count > 1) + _stacks.RemoveFirst(); + } + + internal RuntimeStackStack Clone() + { + var list = new LinkedList>(); + foreach (var stack in _stacks) + list.AddLast(new Stack(stack.Reverse())); + return new RuntimeStackStack(list); + } + } + + private sealed class RuntimeIp + { + internal RuntimeIp(int id) + { + Id = id; + Delta = (1, 0, 0); + StackStack = new RuntimeStackStack(); + } + + internal int Id { get; } + internal (int X, int Y, int Z) Position; + internal (int X, int Y, int Z) Delta; + internal (int X, int Y, int Z) Offset; + internal RuntimeStackStack StackStack; + internal bool StringMode; + internal bool IsStopped; + + internal RuntimeIp CreateChild(int newId) + { + return new RuntimeIp(newId) + { + Position = Position, + Delta = (-Delta.X, -Delta.Y, -Delta.Z), + Offset = Offset, + StackStack = StackStack.Clone(), + StringMode = StringMode, + }; + } + } + internal static int Run( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -33,40 +107,88 @@ internal static int Run( bool hasInput, bool hasOutput) { - int px = 0, py = 0, pz = 0, dx = 1, dy = 0, dz = 0; - bool stringMode = false; - var stack = new List(); var rng = new Random(); + var commandLineArguments = Environment.GetCommandLineArgs(); + var environmentVariables = Environment.GetEnvironmentVariables() + .Cast() + .Select(static entry => string.Concat(entry.Key, "=", entry.Value)) + .ToArray(); + + int nextIpId = 1; int exitCode = 0; + bool quit = false; - int GetCell(int x, int y, int z) => cells.TryGetValue((x, y, z), out var v) ? v : ' '; - void SetCell(int x, int y, int z, int val) + int GetCell(int x, int y, int z) { - if (val == ' ') cells.Remove((x, y, z)); - else cells[(x, y, z)] = val; + int value; + return cells.TryGetValue((x, y, z), out value) ? value : ' '; } - int Pop() { if (stack.Count == 0) return 0; var v = stack[stack.Count - 1]; stack.RemoveAt(stack.Count - 1); return v; } - void Push(int v) => stack.Add(v); - (int x, int y, int z) PopVector() + void SetCell(int x, int y, int z, int value) { - int z = Pop(), y = Pop(), x = Pop(); + if (value == ' ') + cells.Remove((x, y, z)); + else + cells[(x, y, z)] = value; + } + + (int X, int Y, int Z) Advance((int X, int Y, int Z) pos, (int X, int Y, int Z) delta) + { + if (maxX < minX) + return pos; + + int nx = pos.X + delta.X; + int ny = pos.Y + delta.Y; + int nz = pos.Z + delta.Z; + + int width = maxX - minX + 1; + int height = maxY - minY + 1; + int depth = maxZ - minZ + 1; + + if (nx < minX) + nx = maxX - ((minX - nx - 1) % width); + else if (nx > maxX) + nx = minX + ((nx - maxX - 1) % width); + + if (ny < minY) + ny = maxY - ((minY - ny - 1) % height); + else if (ny > maxY) + ny = minY + ((ny - maxY - 1) % height); + + if (nz < minZ) + nz = maxZ - ((minZ - nz - 1) % depth); + else if (nz > maxZ) + nz = minZ + ((nz - maxZ - 1) % depth); + + return (nx, ny, nz); + } + + bool IsSgmlSpace(int c) + { + return c == ' ' || c == '\t' || c == '\f' || c == '\v'; + } + + (int X, int Y, int Z) PopVector(RuntimeStackStack stack) + { + int z = stack.Pop(); + int y = stack.Pop(); + int x = stack.Pop(); return (x, y, z); } - void PushVector(int x, int y, int z) + void PushVector(RuntimeStackStack stack, (int X, int Y, int Z) vector) { - Push(x); - Push(y); - Push(z); + stack.Push(vector.X); + stack.Push(vector.Y); + stack.Push(vector.Z); } - bool TryPopZeroTerminatedString(out string result) + bool TryPopZeroTerminatedString(RuntimeStackStack stack, out string result) { var chars = new List(); while (true) { - int value = Pop(); + int value = stack.Pop(); if (value == 0) { result = new string(chars.ToArray()); @@ -83,7 +205,7 @@ bool TryPopZeroTerminatedString(out string result) } } - bool TryInputFile(int baseX, int baseY, int baseZ, string fileName, bool binaryMode, out (int x, int y, int z) size) + bool TryInputFile((int X, int Y, int Z) leastPoint, string fileName, bool binaryMode, out (int X, int Y, int Z) size) { size = (0, 0, 0); @@ -98,8 +220,8 @@ bool TryInputFile(int baseX, int baseY, int baseZ, string fileName, bool binaryM } int x = 0, y = 0, z = 0; - int maxX = 0, maxY = 0, maxZ = 0; bool wroteAny = false; + int maxVX = 0, maxVY = 0, maxVZ = 0; foreach (byte raw in bytes) { @@ -122,29 +244,29 @@ bool TryInputFile(int baseX, int baseY, int baseZ, string fileName, bool binaryM z++; continue; } - if (cell is '\t' or '\v') + if (cell == '\t' || cell == '\v') cell = ' '; } if (binaryMode || cell != ' ') - SetCell(baseX + x, baseY + y, baseZ + z, cell); + SetCell(leastPoint.X + x, leastPoint.Y + y, leastPoint.Z + z, cell); wroteAny = true; - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; - if (z > maxZ) maxZ = z; + if (x > maxVX) maxVX = x; + if (y > maxVY) maxVY = y; + if (z > maxVZ) maxVZ = z; x++; } - size = wroteAny ? (maxX, maxY, maxZ) : (0, 0, 0); + size = wroteAny ? (maxVX, maxVY, maxVZ) : (0, 0, 0); return true; } - bool TryOutputFile(int baseX, int baseY, int baseZ, int sx, int sy, int sz, string fileName, bool linearText) + bool TryOutputFile((int X, int Y, int Z) leastPoint, (int X, int Y, int Z) size, string fileName, bool linearText) { - sx = Math.Max(0, sx); - sy = Math.Max(0, sy); - sz = Math.Max(0, sz); + int sx = Math.Max(0, size.X); + int sy = Math.Max(0, size.Y); + int sz = Math.Max(0, size.Z); var rows = new List(); for (int z = 0; z <= sz; z++) @@ -154,11 +276,11 @@ bool TryOutputFile(int baseX, int baseY, int baseZ, int sx, int sy, int sz, stri var chars = new char[sx + 1]; for (int x = 0; x <= sx; x++) { - int c = GetCell(baseX + x, baseY + y, baseZ + z); - chars[x] = c is >= char.MinValue and <= char.MaxValue ? (char)c : ' '; + int c = GetCell(leastPoint.X + x, leastPoint.Y + y, leastPoint.Z + z); + chars[x] = c >= char.MinValue && c <= char.MaxValue ? (char)c : ' '; } - var row = new string(chars); + string row = new string(chars); rows.Add(linearText ? row.TrimEnd(' ') : row); } @@ -172,8 +294,8 @@ bool TryOutputFile(int baseX, int baseY, int baseZ, int sx, int sy, int sz, stri rows.RemoveAt(rows.Count - 1); } - var text = string.Join("\n", rows); - var bytes = new byte[text.Length]; + string text = string.Join("\n", rows); + byte[] bytes = new byte[text.Length]; for (int i = 0; i < text.Length; i++) bytes[i] = (byte)(text[i] & 0xFF); @@ -232,209 +354,600 @@ int ExecuteSystemCommand(string command) } } - (int nx, int ny, int nz) Advance(int x, int y, int z, int ddx, int ddy, int ddz) + void PushSysInfo(RuntimeIp ip, int ipCount, int c) { - if (maxX < minX) return (x, y, z); - int nx = x + ddx, ny = y + ddy, nz = z + ddz; - int w = maxX - minX + 1, h = maxY - minY + 1, d = maxZ - minZ + 1; - if (nx < minX) nx = maxX - ((minX - nx - 1) % w); - else if (nx > maxX) nx = minX + ((nx - maxX - 1) % w); - if (ny < minY) ny = maxY - ((minY - ny - 1) % h); - else if (ny > maxY) ny = minY + ((ny - maxY - 1) % h); - if (nz < minZ) nz = maxZ - ((minZ - nz - 1) % d); - else if (nz > maxZ) nz = minZ + ((nz - maxZ - 1) % d); - return (nx, ny, nz); - } + var items = new List(); + + items.Add(0x01 | 0x02 | 0x04 | 0x08); + items.Add(4); + items.Add(unchecked((int)0x46756E67u)); + items.Add(9800); + items.Add(1); + items.Add(Path.DirectorySeparatorChar); + items.Add(3); + items.Add(ip.Id); + items.Add(0); + + items.Add(ip.Position.X); + items.Add(ip.Position.Y); + items.Add(ip.Position.Z); + + items.Add(ip.Delta.X); + items.Add(ip.Delta.Y); + items.Add(ip.Delta.Z); + + items.Add(ip.Offset.X); + items.Add(ip.Offset.Y); + items.Add(ip.Offset.Z); + + items.Add(minX); + items.Add(minY); + items.Add(minZ); + + items.Add(maxX - minX); + items.Add(maxY - minY); + items.Add(maxZ - minZ); + + var now = DateTime.Now; + items.Add(((now.Year - 1900) * 256 * 256) + (now.Month * 256) + now.Day); + items.Add((now.Hour * 256 * 256) + (now.Minute * 256) + now.Second); + + items.Add(ip.StackStack.StackCount); + foreach (var stack in ip.StackStack.AllStacks) + items.Add(stack.Count); + + foreach (var arg in commandLineArguments) + { + foreach (var ch in arg) + items.Add(ch); + items.Add(0); + } + items.Add(0); - bool IsSgmlSpace(int c) => c is ' ' or '\t' or '\f' or '\v'; + foreach (var env in environmentVariables) + { + foreach (var ch in env) + items.Add(ch); + items.Add(0); + } + items.Add(0); + + for (int i = items.Count - 1; i >= 0; i--) + ip.StackStack.Push(items[i]); + + if (c > 0) + { + var snapshot = ip.StackStack.TOSS.ToArray(); + int picked = c <= snapshot.Length ? snapshot[c - 1] : 0; + for (int i = 0; i < items.Count; i++) + ip.StackStack.Pop(); + ip.StackStack.Push(picked); + } + } - bool stopped = false; + var ips = new LinkedList(); + ips.AddFirst(new RuntimeIp(0)); - void ExecuteInstruction(int cell, ref bool suppressAdvance) + void ExecuteInstruction(RuntimeIp ip, LinkedListNode ipNode, ref bool suppressAdvance, int? overrideCell) { + int cell = overrideCell ?? GetCell(ip.Position.X, ip.Position.Y, ip.Position.Z); + + if (ip.StringMode) + { + if (cell == '"') + { + ip.StringMode = false; + } + else if (IsSgmlSpace(cell)) + { + ip.StackStack.Push(' '); + while (true) + { + var next = Advance(ip.Position, ip.Delta); + int nextCell = GetCell(next.X, next.Y, next.Z); + if (IsSgmlSpace(nextCell)) + ip.Position = next; + else + break; + } + } + else + { + ip.StackStack.Push(cell); + } + return; + } + switch (cell) { - case ' ': case '\t': case '\f': case '\v': case 'z': break; - case '!': Push(Pop() == 0 ? 1 : 0); break; - case '$': Pop(); break; - case ':': { int v = Pop(); Push(v); Push(v); break; } - case '\\': { int b = Pop(), a = Pop(); Push(b); Push(a); break; } - case 'n': stack.Clear(); break; - case '+': { int b = Pop(), a = Pop(); Push(a + b); break; } - case '-': { int b = Pop(), a = Pop(); Push(a - b); break; } - case '*': { int b = Pop(), a = Pop(); Push(a * b); break; } - case '/': { int b = Pop(), a = Pop(); Push(b == 0 ? 0 : a / b); break; } - case '%': { int b = Pop(), a = Pop(); Push(b == 0 ? 0 : a % b); break; } - case '`': { int b = Pop(), a = Pop(); Push(a > b ? 1 : 0); break; } + case ' ': case '\t': case '\f': case '\v': case 'z': + break; + + case '!': + ip.StackStack.Push(ip.StackStack.Pop() == 0 ? 1 : 0); + break; + + case '$': + ip.StackStack.Pop(); + break; + + case ':': + { + int v = ip.StackStack.Pop(); + ip.StackStack.Push(v); + ip.StackStack.Push(v); + break; + } + + case '\\': + { + int b = ip.StackStack.Pop(); + int a = ip.StackStack.Pop(); + ip.StackStack.Push(b); + ip.StackStack.Push(a); + break; + } + + case 'n': + ip.StackStack.ClearToss(); + break; + + case '+': + { + int b = ip.StackStack.Pop(); + int a = ip.StackStack.Pop(); + ip.StackStack.Push(a + b); + break; + } + + case '-': + { + int b = ip.StackStack.Pop(); + int a = ip.StackStack.Pop(); + ip.StackStack.Push(a - b); + break; + } + + case '*': + { + int b = ip.StackStack.Pop(); + int a = ip.StackStack.Pop(); + ip.StackStack.Push(a * b); + break; + } + + case '/': + { + int b = ip.StackStack.Pop(); + int a = ip.StackStack.Pop(); + ip.StackStack.Push(b == 0 ? 0 : a / b); + break; + } + + case '%': + { + int b = ip.StackStack.Pop(); + int a = ip.StackStack.Pop(); + ip.StackStack.Push(b == 0 ? 0 : a % b); + break; + } + + case '`': + { + int b = ip.StackStack.Pop(); + int a = ip.StackStack.Pop(); + ip.StackStack.Push(a > b ? 1 : 0); + break; + } + case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': Push(cell - '0'); break; - case 'a': Push(10); break; case 'b': Push(11); break; case 'c': Push(12); break; - case 'd': Push(13); break; case 'e': Push(14); break; case 'f': Push(15); break; - case '>': dx = 1; dy = 0; dz = 0; break; - case '<': dx = -1; dy = 0; dz = 0; break; - case '^': dx = 0; dy = -1; dz = 0; break; - case 'v': dx = 0; dy = 1; dz = 0; break; - case 'h': dx = 0; dy = 0; dz = -1; break; - case 'l': dx = 0; dy = 0; dz = 1; break; + case '5': case '6': case '7': case '8': case '9': + ip.StackStack.Push(cell - '0'); + break; + case 'a': ip.StackStack.Push(10); break; + case 'b': ip.StackStack.Push(11); break; + case 'c': ip.StackStack.Push(12); break; + case 'd': ip.StackStack.Push(13); break; + case 'e': ip.StackStack.Push(14); break; + case 'f': ip.StackStack.Push(15); break; + + case '>': ip.Delta = (1, 0, 0); break; + case '<': ip.Delta = (-1, 0, 0); break; + case '^': ip.Delta = (0, -1, 0); break; + case 'v': ip.Delta = (0, 1, 0); break; + case 'h': ip.Delta = (0, 0, -1); break; + case 'l': ip.Delta = (0, 0, 1); break; case '?': switch (rng.Next(6)) { - case 0: dx=1;dy=0;dz=0;break; - case 1: dx=-1;dy=0;dz=0;break; - case 2: dx=0;dy=-1;dz=0;break; - case 3: dx=0;dy=1;dz=0;break; - case 4: dx=0;dy=0;dz=-1;break; - default: dx=0;dy=0;dz=1;break; + case 0: ip.Delta = (1, 0, 0); break; + case 1: ip.Delta = (-1, 0, 0); break; + case 2: ip.Delta = (0, -1, 0); break; + case 3: ip.Delta = (0, 1, 0); break; + case 4: ip.Delta = (0, 0, -1); break; + default: ip.Delta = (0, 0, 1); break; } break; - case 'm': { int v = Pop(); dx = 0; dy = 0; dz = v == 0 ? 1 : -1; break; } - case '_': { int v = Pop(); dx = v == 0 ? 1 : -1; dy = 0; dz = 0; break; } - case '|': { int v = Pop(); dy = v == 0 ? 1 : -1; dx = 0; dz = 0; break; } - case '[': { int ndx = dy, ndy = -dx; dx = ndx; dy = ndy; dz = 0; break; } - case ']': { int ndx = -dy, ndy = dx; dx = ndx; dy = ndy; dz = 0; break; } - case 'r': dx = -dx; dy = -dy; dz = -dz; break; - case 'x': { int ndz = Pop(), ndy = Pop(), ndx = Pop(); dx = ndx; dy = ndy; dz = ndz; break; } - case 'w': { int b = Pop(), a = Pop(); if(a>b){int ndx=-dy,ndy=dx;dx=ndx;dy=ndy;dz=0;}else if(a b) + ip.Delta = (-ip.Delta.Y, ip.Delta.X, 0); + else if (a < b) + ip.Delta = (ip.Delta.Y, -ip.Delta.X, 0); + break; + } + + case '#': + ip.Position = Advance(ip.Position, ip.Delta); + break; case 'j': - { - int s = Pop(); int jdx = s >= 0 ? dx : -dx, jdy = s >= 0 ? dy : -dy, jdz = s >= 0 ? dz : -dz, abs = s < 0 ? -s : s; - for (int i = 0; i < abs; i++) (px, py, pz) = Advance(px, py, pz, jdx, jdy, jdz); - suppressAdvance = true; break; - } + { + int s = ip.StackStack.Pop(); + var step = s >= 0 ? ip.Delta : (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + int count = Math.Abs(s); + for (int i = 0; i < count; i++) + ip.Position = Advance(ip.Position, step); + suppressAdvance = true; + break; + } case ';': - (px, py, pz) = Advance(px, py, pz, dx, dy, dz); - while (GetCell(px, py, pz) != ';') (px, py, pz) = Advance(px, py, pz, dx, dy, dz); + ip.Position = Advance(ip.Position, ip.Delta); + while (GetCell(ip.Position.X, ip.Position.Y, ip.Position.Z) != ';') + ip.Position = Advance(ip.Position, ip.Delta); break; - case '\'': (px, py, pz) = Advance(px, py, pz, dx, dy, dz); Push(GetCell(px, py, pz)); break; - case 's': { int sv = Pop(); (px, py, pz) = Advance(px, py, pz, dx, dy, dz); SetCell(px, py, pz, sv); break; } - case '"': stringMode = true; break; - case 'g': { int gz = Pop(), gy = Pop(), gx = Pop(); Push(GetCell(gx, gy, gz)); break; } - case 'p': { int gz = Pop(), gy = Pop(), gx = Pop(), pv = Pop(); SetCell(gx, gy, gz, pv); break; } + + case '\'': + ip.Position = Advance(ip.Position, ip.Delta); + ip.StackStack.Push(GetCell(ip.Position.X, ip.Position.Y, ip.Position.Z)); + break; + case 's': + { + int sv = ip.StackStack.Pop(); + ip.Position = Advance(ip.Position, ip.Delta); + SetCell(ip.Position.X, ip.Position.Y, ip.Position.Z, sv); + break; + } + case '"': + ip.StringMode = true; + break; + + case 'g': + { + int z = ip.StackStack.Pop(); + int y = ip.StackStack.Pop(); + int x = ip.StackStack.Pop(); + ip.StackStack.Push(GetCell(x + ip.Offset.X, y + ip.Offset.Y, z + ip.Offset.Z)); + break; + } + case 'p': + { + int z = ip.StackStack.Pop(); + int y = ip.StackStack.Pop(); + int x = ip.StackStack.Pop(); + int v = ip.StackStack.Pop(); + SetCell(x + ip.Offset.X, y + ip.Offset.Y, z + ip.Offset.Z, v); + break; + } + case '.': - if (!hasOutput) throw new InvalidOperationException("Funge output instruction '.' executed without an output interface."); - output.Write(Pop()); output.Write(' '); break; + if (!hasOutput) + throw new InvalidOperationException("Funge output instruction '.' executed without an output interface."); + output.Write(ip.StackStack.Pop()); + output.Write(' '); + break; case ',': - if (!hasOutput) throw new InvalidOperationException("Funge output instruction ',' executed without an output interface."); - output.Write((char)Pop()); break; + if (!hasOutput) + throw new InvalidOperationException("Funge output instruction ',' executed without an output interface."); + output.Write((char)ip.StackStack.Pop()); + break; case '&': - { - if (!hasInput) throw new InvalidOperationException("Funge input instruction '&' executed without an input interface."); - var line = input.ReadLine(); if(line==null){dx=-dx;dy=-dy;dz=-dz;}else Push(int.TryParse(line.Trim(),out int iv)?iv:0); break; - } + { + if (!hasInput) + throw new InvalidOperationException("Funge input instruction '&' executed without an input interface."); + var line = input.ReadLine(); + if (line == null) + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + else + { + int v; + ip.StackStack.Push(int.TryParse(line.Trim(), out v) ? v : 0); + } + break; + } case '~': - { - if (!hasInput) throw new InvalidOperationException("Funge input instruction '~' executed without an input interface."); - int ch = input.Read(); if(ch<0){dx=-dx;dy=-dy;dz=-dz;}else Push(ch); break; - } + { + if (!hasInput) + throw new InvalidOperationException("Funge input instruction '~' executed without an input interface."); + int ch = input.Read(); + if (ch < 0) + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + else + ip.StackStack.Push(ch); + break; + } + case 'i': - { - if (!TryPopZeroTerminatedString(out string fileName)) { dx = -dx; dy = -dy; dz = -dz; break; } - int flags = Pop(); - var (vx, vy, vz) = PopVector(); - int baseX = vx, baseY = vy, baseZ = vz; - bool binaryMode = (flags & 1) != 0; - if (!TryInputFile(baseX, baseY, baseZ, fileName, binaryMode, out var vb)) { dx = -dx; dy = -dy; dz = -dz; break; } - PushVector(vx, vy, vz); - PushVector(vb.x, vb.y, vb.z); - break; - } + { + string fileName; + if (!TryPopZeroTerminatedString(ip.StackStack, out fileName)) + { + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + break; + } + + int flags = ip.StackStack.Pop(); + var va = PopVector(ip.StackStack); + va = (va.X + ip.Offset.X, va.Y + ip.Offset.Y, va.Z + ip.Offset.Z); + bool binaryMode = (flags & 1) != 0; + + (int X, int Y, int Z) vb; + if (!TryInputFile(va, fileName, binaryMode, out vb)) + { + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + break; + } + + PushVector(ip.StackStack, (va.X - ip.Offset.X, va.Y - ip.Offset.Y, va.Z - ip.Offset.Z)); + PushVector(ip.StackStack, vb); + break; + } + case 'o': - { - if (!TryPopZeroTerminatedString(out string fileName)) { dx = -dx; dy = -dy; dz = -dz; break; } - int flags = Pop(); - var (sbx, sby, sbz) = PopVector(); - var (vax, vay, vaz) = PopVector(); - bool linearText = (flags & 1) != 0; - if (!TryOutputFile(vax, vay, vaz, sbx, sby, sbz, fileName, linearText)) { dx = -dx; dy = -dy; dz = -dz; } + { + string fileName; + if (!TryPopZeroTerminatedString(ip.StackStack, out fileName)) + { + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + break; + } + + int flags = ip.StackStack.Pop(); + var vb = PopVector(ip.StackStack); + var va = PopVector(ip.StackStack); + va = (va.X + ip.Offset.X, va.Y + ip.Offset.Y, va.Z + ip.Offset.Z); + bool linearText = (flags & 1) != 0; + + if (!TryOutputFile(va, vb, fileName, linearText)) + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + break; + } + + case '@': + ip.IsStopped = true; break; - } - case '=': - { - if (!TryPopZeroTerminatedString(out string command)) { dx = -dx; dy = -dy; dz = -dz; break; } - Push(ExecuteSystemCommand(command)); + case 'q': + exitCode = ip.StackStack.Pop(); + quit = true; break; - } + case 'k': - { - int n = Pop(); - var (ix, iy, iz) = Advance(px, py, pz, dx, dy, dz); - while (true) { - int c = GetCell(ix, iy, iz); - if (IsSgmlSpace(c)) + int n = ip.StackStack.Pop(); + var instrPos = Advance(ip.Position, ip.Delta); + while (true) { - (ix, iy, iz) = Advance(ix, iy, iz, dx, dy, dz); + int c = GetCell(instrPos.X, instrPos.Y, instrPos.Z); + if (IsSgmlSpace(c)) + { + instrPos = Advance(instrPos, ip.Delta); + } + else if (c == ';') + { + instrPos = Advance(instrPos, ip.Delta); + while (GetCell(instrPos.X, instrPos.Y, instrPos.Z) != ';') + instrPos = Advance(instrPos, ip.Delta); + instrPos = Advance(instrPos, ip.Delta); + } + else + { + break; + } } - else if (c == ';') + + if (n == 0) { - (ix, iy, iz) = Advance(ix, iy, iz, dx, dy, dz); - while (GetCell(ix, iy, iz) != ';') (ix, iy, iz) = Advance(ix, iy, iz, dx, dy, dz); - (ix, iy, iz) = Advance(ix, iy, iz, dx, dy, dz); + ip.Position = instrPos; } - else + else if (n > 0) { - break; + int operand = GetCell(instrPos.X, instrPos.Y, instrPos.Z); + for (int i = 0; i < n && !ip.IsStopped && !quit; i++) + { + bool dummy = false; + ExecuteInstruction(ip, ipNode, ref dummy, operand); + } } + break; } - if (n == 0) + case 't': { - (px, py, pz) = (ix, iy, iz); + var child = ip.CreateChild(nextIpId++); + ips.AddAfter(ipNode, child); + break; } - else + + case '{': { - int operand = GetCell(ix, iy, iz); - for (int i = 0; i < n && !stopped; i++) + int n = ip.StackStack.Pop(); + + var items = new List(); + if (n > 0) { - bool dummy = false; - ExecuteInstruction(operand, ref dummy); + for (int i = 0; i < n; i++) + items.Add(ip.StackStack.Pop()); } + + ip.StackStack.Push(ip.Offset.X); + ip.StackStack.Push(ip.Offset.Y); + ip.StackStack.Push(ip.Offset.Z); + + ip.StackStack.PushNewStack(); + + if (n > 0) + { + for (int i = items.Count - 1; i >= 0; i--) + ip.StackStack.Push(items[i]); + } + else if (n < 0) + { + var soss = ip.StackStack.SOSS; + for (int i = 0; i < -n; i++) + soss.Push(0); + } + + ip.Offset = Advance(ip.Position, ip.Delta); + break; } - break; - } - case '@': stopped = true; break; - case 'q': exitCode = Pop(); stopped = true; break; - default: if (cell >= 'A' && cell <= 'Z') { dx = -dx; dy = -dy; dz = -dz; } break; - } - } - while (!stopped) - { - int cell = GetCell(px, py, pz); - if (stringMode) - { - if (cell == '"') - { - stringMode = false; - } - else if (IsSgmlSpace(cell)) - { - Push(' '); - while (true) + case '}': { - var (nx, ny, nz) = Advance(px, py, pz, dx, dy, dz); - if (IsSgmlSpace(GetCell(nx, ny, nz))) + int n = ip.StackStack.Pop(); + if (!ip.StackStack.HasSOSS) { - (px, py, pz) = (nx, ny, nz); + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + break; } - else + + var items = new List(); + for (int i = 0; i < Math.Max(0, n); i++) + items.Add(ip.StackStack.Pop()); + + ip.StackStack.PopCurrentStack(); + + int oz = ip.StackStack.Pop(); + int oy = ip.StackStack.Pop(); + int ox = ip.StackStack.Pop(); + ip.Offset = (ox, oy, oz); + + if (n < 0) + { + for (int i = 0; i < -n; i++) + ip.StackStack.Pop(); + } + + for (int i = items.Count - 1; i >= 0; i--) + ip.StackStack.Push(items[i]); + break; + } + + case 'u': + { + int n = ip.StackStack.Pop(); + if (!ip.StackStack.HasSOSS) + { + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + break; + } + + var soss = ip.StackStack.SOSS; + if (n > 0) + { + for (int i = 0; i < n; i++) + ip.StackStack.Push(soss.Count > 0 ? soss.Pop() : 0); + } + else if (n < 0) { + for (int i = 0; i < -n; i++) + soss.Push(ip.StackStack.Pop()); + } + break; + } + + case 'y': + { + int c = ip.StackStack.Pop(); + PushSysInfo(ip, ips.Count, c); + break; + } + + case '(': + case ')': + { + int n = ip.StackStack.Pop(); + for (int i = 0; i < n; i++) + ip.StackStack.Pop(); + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + break; + } + + case '=': + { + string command; + if (!TryPopZeroTerminatedString(ip.StackStack, out command)) + { + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); break; } + + ip.StackStack.Push(ExecuteSystemCommand(command)); + break; } + + default: + if (cell >= 'A' && cell <= 'Z') + ip.Delta = (-ip.Delta.X, -ip.Delta.Y, -ip.Delta.Z); + break; + } + } + + while (ips.Count > 0 && !quit) + { + var node = ips.First; + while (node != null && !quit) + { + var nextNode = node.Next; + var ip = node.Value; + + bool suppressAdvance = false; + ExecuteInstruction(ip, node, ref suppressAdvance, null); + + if (ip.IsStopped || quit) + { + ips.Remove(node); } - else + else if (!suppressAdvance) { - Push(cell); + ip.Position = Advance(ip.Position, ip.Delta); } - (px, py, pz) = Advance(px, py, pz, dx, dy, dz); - continue; - } - bool suppressAdvance = false; - ExecuteInstruction(cell, ref suppressAdvance); - if (!stopped && !suppressAdvance) (px, py, pz) = Advance(px, py, pz, dx, dy, dz); + node = nextNode; + } } return exitCode; diff --git a/Generator/README.md b/Generator/README.md index 4050af8..3ac6513 100644 --- a/Generator/README.md +++ b/Generator/README.md @@ -130,8 +130,8 @@ The generated runtime automatically handles XYZ coordinates, Z-axis wrapping, an ## Funge-98 Compliance -The generated runtime (`FungeRuntime`) implements a **single-IP, single-stack subset** of Funge-98, -including Trefunge 3D navigation (`h` / `l` / `m`) but excluding concurrency, stack stack operations, and fingerprints. +The generated runtime (`FungeRuntime`) implements Funge-98 core execution including Trefunge 3D navigation (`h` / `l` / `m`), +concurrency (`t`), and stack stack operations (`{` / `}` / `u`), while still excluding fingerprints. | Category | Instructions | Status | | --- | --- | --- | @@ -142,14 +142,14 @@ including Trefunge 3D navigation (`h` / `l` / `m`) but excluding concurrency, st | Branching | `_` `\|` | ✅ | | Movement | `#` `;` `j` | ✅ | | String / char | `"` `'` `s` | ✅ (stringmode contiguous spaces are SGML-style) | -| Storage (self-modifying) | `g` `p` | 🟡 storage offset not applied | +| Storage (self-modifying) | `g` `p` (with storage offset) | ✅ | | I/O | `.` `,` `&` `~` | ✅ | | Misc | `z` `@` | ✅ | | Exit code | `q` | ✅ pops stack top and returns it as method exit code (`@` returns `0`) | | Iteration | `k` | ✅ | -| Concurrency | `t` | ❌ not implemented (single IP only) | -| Stack stack | `{` `}` `u` | ❌ not implemented | -| System info | `y` | ❌ not implemented | +| Concurrency | `t` | ✅ | +| Stack stack | `{` `}` `u` | ✅ | +| System info | `y` | ✅ | | File I/O | `i` `o` | ✅ | | System exec | `=` | ✅ | | Fingerprints | `(` `)` `A`–`Z` | ❌ reflects (not implemented) | diff --git a/Interpreter/FungeInterpreterExtensions.cs b/Interpreter/FungeInterpreterExtensions.cs index 917c6cd..12c14bd 100644 --- a/Interpreter/FungeInterpreterExtensions.cs +++ b/Interpreter/FungeInterpreterExtensions.cs @@ -1,6 +1,7 @@ using Esolang.Funge.Parser; using Esolang.Funge.Processor; using System.CommandLine; +using System.Collections; namespace Esolang.Funge.Interpreter; @@ -28,7 +29,15 @@ public static RootCommand BuildRootCommand() { var path = parseResult.GetValue(pathArgument)!; var space = FungeParser.ParseFile(path); - var proc = new FungeProcessor(space, Console.Out, Console.In); + var env = Environment.GetEnvironmentVariables() + .Cast() + .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)); }); diff --git a/Interpreter/README.md b/Interpreter/README.md index 8c005d2..dcde688 100644 --- a/Interpreter/README.md +++ b/Interpreter/README.md @@ -37,7 +37,7 @@ For detailed processor-level behavior, refer to the processor package documentat | --- | --- | | Core instruction set (stack, arithmetic, comparison, direction, I/O, storage, movement) | ✅ | | Funge-98 extensions (`k` iterate, `t` concurrency, `{`/`}`/`u` stack stack) | ✅ | -| System info (`y`) | 🟡 env vars / command-line args are empty | +| System info (`y`) | ✅ | | Standard I/O (`&` `~` `,` `.`) connected to stdin / stdout | ✅ | | Exit code via `q` | ✅ | | Fingerprints (`(` `)` `A`–`Z`) | ❌ reflects (not implemented) | diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index 03cfbd6..80d9463 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -1,6 +1,7 @@ using Esolang.Funge.Parser; using Esolang.Processor; using System.Diagnostics; +using System.Collections; namespace Esolang.Funge.Processor; @@ -16,6 +17,8 @@ public sealed partial class FungeProcessor : ITextProcessor private readonly FungeSpace _space; private readonly TextWriter _output; private readonly TextReader _input; + private readonly string[] _commandLineArguments; + private readonly string[] _environmentVariables; private readonly Random _random = new(); private int _nextIpId; @@ -28,11 +31,23 @@ public sealed partial class FungeProcessor : ITextProcessor /// The parsed Funge-98 program space. /// Output writer; defaults to . /// Input reader; defaults to . - public FungeProcessor(FungeSpace space, TextWriter? output = null, TextReader? input = null) + /// 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 FungeProcessor( + FungeSpace space, + TextWriter? output = null, + TextReader? input = null, + IEnumerable? commandLineArguments = null, + IEnumerable? environmentVariables = null) { _space = space; _output = output ?? Console.Out; _input = input ?? Console.In; + _commandLineArguments = (commandLineArguments ?? Environment.GetCommandLineArgs()).ToArray(); + _environmentVariables = (environmentVariables ?? Environment.GetEnvironmentVariables() + .Cast() + .Select(static entry => $"{entry.Key}={entry.Value}")) + .ToArray(); } /// @@ -843,24 +858,39 @@ private void PushSysInfo(InstructionPointer ip, int _, int c) items.Add(_space.MinX); items.Add(_space.MinY); items.Add(_space.MinZ); - // 22-24. Greatest point of LSAB (maxX, maxY, maxZ; maxZ on top) - items.Add(_space.MaxX); - items.Add(_space.MaxY); - items.Add(_space.MaxZ); + // 22-24. Greatest point relative to least point (max-min) + items.Add(_space.MaxX - _space.MinX); + items.Add(_space.MaxY - _space.MinY); + items.Add(_space.MaxZ - _space.MinZ); var now = DateTime.Now; - // 20. Current date: (year-1900)*10000 + month*100 + day - items.Add(((now.Year - 1900) * 10000) + (now.Month * 100) + now.Day); - // 21. Current time: HH*10000 + MM*100 + SS - items.Add((now.Hour * 10000) + (now.Minute * 100) + now.Second); + // 20. Current date: (year-1900)*256*256 + month*256 + 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); // 22. Number of stacks in stack stack items.Add(ip.StackStack.StackCount); // 23+. Size of each stack (TOSS first) foreach (var stack in ip.StackStack.AllStacks) items.Add(stack.Count); - // Command-line args: empty list (single 0 terminator) + // Command-line args: null-terminated strings, series terminated by extra null. + foreach (var arg in _commandLineArguments) + { + foreach (var ch in arg) + items.Add(ch); + items.Add(0); + } + // Series terminator items.Add(0); - // Environment variables: empty list (single 0 terminator) + + // Environment variables: null-terminated strings, series terminated by extra null. + foreach (var env in _environmentVariables) + { + foreach (var ch in env) + items.Add(ch); + items.Add(0); + } + // Series terminator items.Add(0); // Push in reverse order so items[0] ends up on top (= item 1) @@ -869,11 +899,15 @@ private void PushSysInfo(InstructionPointer ip, int _, int c) if (c > 0) { - // Pick the c-th item from top of pushed items - var popped = new int[items.Count]; + // y with positive c: keep only the c-th item from the top of the full stack. + // This naturally allows "pick" behavior when c exceeds y's own payload length. + var snapshot = ip.StackStack.TOSS.ToArray(); // top-first + var picked = c <= snapshot.Length ? snapshot[c - 1] : 0; + for (var i = 0; i < items.Count; i++) - popped[i] = ip.StackStack.Pop(); - ip.StackStack.Push(c <= items.Count ? popped[c - 1] : 0); + ip.StackStack.Pop(); + + ip.StackStack.Push(picked); } } } diff --git a/Processor/README.md b/Processor/README.md index e726061..3addb62 100644 --- a/Processor/README.md +++ b/Processor/README.md @@ -45,7 +45,7 @@ Targets **Funge-98** with 3D navigation (`h`/`l`/`m`). Fingerprint extensions ar | I/O | `.` `,` `&` `~` | ✅ | | Concurrency | `t` | ✅ | | Stack stack | `{` `}` `u` | ✅ | -| System info | `y` | 🟡 env vars / command-line args are empty | +| System info | `y` | ✅ | | Misc | `z` `@` `q` | ✅ | | File I/O | `i` `o` | ✅ | | System exec | `=` | ✅ | From 2b93686d015e63afdd464fe6e0746b027126c21c Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 09:31:15 +0900 Subject: [PATCH 10/17] Fix runtime nullability and test cancellation token usage --- Generator.Tests/FungeMethodGeneratorTests.cs | 108 +++++++++++++------ Generator/MethodGenerator.Runtime.cs | 4 +- 2 files changed, 76 insertions(+), 36 deletions(-) diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index 3034c6e..9092a94 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -184,14 +184,14 @@ partial class TestClass additionalFiles: [("hello.b98", helloWorld)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (string?)m.Invoke(null, []); Assert.AreEqual("Hello, World!", result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -212,14 +212,14 @@ partial class TestClass additionalFiles: [("sgml.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (string?)m.Invoke(null, []); Assert.AreEqual("32 0 ", result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -240,14 +240,14 @@ partial class TestClass additionalFiles: [("k.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (string?)m.Invoke(null, []); Assert.AreEqual("6 6 6 ", result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -465,6 +465,44 @@ partial class TestClass AssertNoErrors(diag, comp); } + [TestMethod] + public void RuntimeTemplate_NullabilityWarnings_NotEmitted() + { + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("test.b98")] + public static partial void Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("test.b98", "@")]); + AssertNoErrors(diag, comp); + + var options = (CSharpCompilationOptions)comp.Options; + var strict = comp.WithOptions(options.WithSpecificDiagnosticOptions( + options.SpecificDiagnosticOptions + .SetItem("CS8602", ReportDiagnostic.Error) + .SetItem("CS8603", ReportDiagnostic.Error))); + + var runtimeNullabilityErrors = strict + .GetDiagnostics(TestCancellationToken) + .Where(static d => d.Severity == DiagnosticSeverity.Error) + .Where(static d => d.Id is "CS8602" or "CS8603") + .Where(static d => d.Location.SourceTree?.FilePath.Contains("FungeRuntime.g.cs", StringComparison.OrdinalIgnoreCase) == true) + .ToArray(); + + if (runtimeNullabilityErrors.Length > 0) + { + foreach (var d in runtimeNullabilityErrors) + TestContext.WriteLine(d.ToString()); + } + + Assert.AreEqual(0, runtimeNullabilityErrors.Length, "FungeRuntime.g.cs must not produce CS8602/CS8603."); + } + // ----------------------------------------------------------------------- // Diagnostic tests // ----------------------------------------------------------------------- @@ -504,14 +542,14 @@ partial class TestClass additionalFiles: [("exit-code.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (int?)m.Invoke(null, []); Assert.AreEqual(5, result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -532,14 +570,14 @@ partial class TestClass additionalFiles: [("exit-code-zero.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (int?)m.Invoke(null, []); Assert.AreEqual(0, result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -560,14 +598,14 @@ partial class TestClass additionalFiles: [("go-low.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (int?)m.Invoke(null, []); Assert.AreEqual(7, result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -588,14 +626,14 @@ partial class TestClass additionalFiles: [("go-high.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (int?)m.Invoke(null, []); Assert.AreEqual(7, result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -620,7 +658,7 @@ partial class TestClass additionalFiles: [("select-low.b98", programLow), ("select-high.b98", programHigh)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; @@ -630,7 +668,7 @@ await Task.Factory.StartNew(() => var high = (int?)mHigh.Invoke(null, []); Assert.AreEqual(1, low); Assert.AreEqual(2, high); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -651,14 +689,14 @@ partial class TestClass additionalFiles: [("getput-3d.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (int?)m.Invoke(null, []); Assert.AreEqual(65, result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -679,14 +717,14 @@ partial class TestClass additionalFiles: [("offset-getput.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (int?)m.Invoke(null, []); Assert.AreEqual(65, result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -707,14 +745,14 @@ partial class TestClass additionalFiles: [("stack-u.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (string?)m.Invoke(null, []); Assert.AreEqual("2 ", result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -735,14 +773,14 @@ partial class TestClass additionalFiles: [("sysinfo-flags.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (int?)m.Invoke(null, []); Assert.AreEqual(15, result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -773,14 +811,14 @@ partial class TestClass additionalFiles: [("file-in.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (int?)m.Invoke(null, []); Assert.AreEqual(65, result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } finally { @@ -816,13 +854,13 @@ partial class TestClass additionalFiles: [("file-out.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; _ = m.Invoke(null, []); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); var bytes = File.ReadAllBytes(Path.Combine(tempDir, "output.txt")); CollectionAssert.AreEqual(new byte[] { 65 }, bytes); @@ -854,14 +892,14 @@ partial class TestClass additionalFiles: [("system-exec.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (int?)m.Invoke(null, []); Assert.AreEqual(7, result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -884,14 +922,14 @@ partial class TestClass additionalFiles: [("system-exec-fail.b98", program)]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); await Task.Factory.StartNew(() => { var t = asm.GetType("TestProject.TestClass")!; var m = t.GetMethod("Run")!; var result = (int?)m.Invoke(null, []); Assert.AreNotEqual(0, result); - }, TestContext.CancellationTokenSource.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } [TestMethod] @@ -962,7 +1000,7 @@ partial class TestClass additionalFiles: [("test.b98", "68*2-s<<@")]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); var t = asm.GetType("TestProject.TestClass") ?? asm.GetType("TestClass"); Assert.IsNotNull(t, "Failed to find generated type TestProject.TestClass."); @@ -992,7 +1030,7 @@ partial class TestClass additionalFiles: [("test.b98", "66*2+s<<@")]); AssertNoErrors(diag, comp); - var asm = Emit(comp); + var asm = Emit(comp, TestCancellationToken); var t = asm.GetType("TestProject.TestClass") ?? asm.GetType("TestClass"); Assert.IsNotNull(t, "Failed to find generated type TestProject.TestClass."); @@ -1097,3 +1135,5 @@ file sealed class TestAdditionalText(string path, string content) : AdditionalTe public override SourceText? GetText(CancellationToken cancellationToken = default) => SourceText.From(content, Encoding.UTF8); } + + diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index d1e2a94..fbb8c3a 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -43,8 +43,8 @@ private RuntimeStackStack(LinkedList> stacks) _stacks.AddLast(stack); } - internal Stack TOSS { get { return _stacks.First.Value; } } - internal Stack SOSS { get { return _stacks.Count >= 2 ? _stacks.First.Next.Value : null; } } + internal Stack TOSS { get { return _stacks.First!.Value; } } + internal Stack SOSS { get { return _stacks.First!.Next!.Value; } } internal bool HasSOSS { get { return _stacks.Count >= 2; } } internal int StackCount { get { return _stacks.Count; } } internal IEnumerable> AllStacks { get { return _stacks; } } From 96c008305aaaff3c36c6c8fcef975efbc1be4f16 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 09:32:33 +0900 Subject: [PATCH 11/17] dotnet format --- Generator.Tests/FungeMethodGeneratorTests.cs | 2 +- Generator/DiagnosticDescriptors.cs | 242 ++++++------- Interpreter.Tests/ProgramTests.cs | 112 +++--- Interpreter/FungeInterpreterExtensions.cs | 2 +- Interpreter/Program.cs | 80 ++--- Parser.Tests/FungeParserTests.cs | 334 +++++++++--------- Parser/FungeParser.cs | 92 ++--- Parser/FungeSpace.cs | 230 ++++++------ Parser/FungeVector.cs | 180 +++++----- Parser/Shared/HashCode.cs | 30 +- Parser/Shared/IsExternalInit.cs | 16 +- Processor/FungeProcessor.cs | 2 +- Processor/InstructionPointer.cs | 108 +++--- Processor/StackStack.cs | 130 +++---- .../Esolang.Funge.Generator.UseConsole.cs | 124 +++---- 15 files changed, 842 insertions(+), 842 deletions(-) diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index 9092a94..836ff21 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -1071,7 +1071,7 @@ partial class TestClass .Select(static t => t.ToString()) .Single(static text => text.Contains("Generated from: ", StringComparison.Ordinal)); Assert.Contains("__cells[(0, 0, 0)] = 64;", generated); - + // Output all generated syntax trees for inspection TestContext.WriteLine("=== Generated Syntax Trees ==="); foreach (var tree in comp.SyntaxTrees) diff --git a/Generator/DiagnosticDescriptors.cs b/Generator/DiagnosticDescriptors.cs index 501bfde..81ffe09 100644 --- a/Generator/DiagnosticDescriptors.cs +++ b/Generator/DiagnosticDescriptors.cs @@ -1,121 +1,121 @@ -using Microsoft.CodeAnalysis; - -namespace Esolang.Funge.Generator; - -/// -/// Provides diagnostic definitions reported during source generation. -/// -public static class DiagnosticDescriptors -{ - const string Category = "Funge"; - - /// - /// FG0001: Invalid source path parameter. - /// - public static readonly DiagnosticDescriptor InvalidSourcePathParameter = new( - id: "FG0001", - title: "Invalid source path parameter", - messageFormat: "The source path parameter of the attribute on the method '{0}' must not be null or empty", - category: Category, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - /// - /// FG0002: Unsupported return type. - /// - public static readonly DiagnosticDescriptor InvalidReturnType = new( - id: "FG0002", - title: "Unsupported return type", - messageFormat: "The method return type '{0}' is not supported for Funge code generation", - category: Category, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - /// - /// FG0003: Unsupported parameter type. - /// - public static readonly DiagnosticDescriptor InvalidParameter = new( - id: "FG0003", - title: "Unsupported parameter type", - messageFormat: "The parameter '{0}' of the method has an unsupported type", - category: Category, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - /// - /// FG0004: Source file not found. - /// - public static readonly DiagnosticDescriptor SourceFileNotFound = new( - id: "FG0004", - title: "Funge source file not found", - messageFormat: "The Funge source file '{0}' could not be found in AdditionalFiles", - category: Category, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - /// - /// FG0005: Consumer language version is below C# 8.0. - /// - public static readonly DiagnosticDescriptor LanguageVersionTooLow = new( - id: "FG0005", - title: "Language version too low", - messageFormat: "Funge source generation requires C# 8.0 or later (current: {0})", - category: Category, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); - - /// - /// FG0006: Duplicate parameter type. - /// - public static readonly DiagnosticDescriptor DuplicateParameter = new( - id: "FG0006", - title: "Duplicate parameter type", - messageFormat: "The parameter type '{0}' appears more than once in method '{1}'", - category: Category, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - /// - /// FG0007: Return type and output parameter conflict. - /// - public static readonly DiagnosticDescriptor ReturnOutputConflict = new( - id: "FG0007", - title: "Return type and output parameter conflict", - messageFormat: "Method '{0}' has both a non-void return type and an output parameter (TextWriter/PipeWriter); use one or the other", - category: Category, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - /// - /// FG0008: Output interface required. - /// - public static readonly DiagnosticDescriptor RequiredOutputInterface = new( - id: "FG0008", - title: "Output interface required", - messageFormat: "Method '{0}' uses Funge output instructions but has no output (return string/IEnumerable<byte> or a TextWriter/PipeWriter parameter)", - category: Category, - defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true); - - /// - /// FG0009: Input interface required. - /// - public static readonly DiagnosticDescriptor RequiredInputInterface = new( - id: "FG0009", - title: "Input interface required", - messageFormat: "Method '{0}' uses Funge input instructions but has no input (string, TextReader, or PipeReader parameter)", - category: Category, - defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true); - - /// - /// FG0010: Unused input interface. - /// - public static readonly DiagnosticDescriptor UnusedInputInterface = new( - id: "FG0010", - title: "Unused input interface", - messageFormat: "Method '{0}' has an input parameter but the Funge source does not use any input instructions", - category: Category, - defaultSeverity: DiagnosticSeverity.Hidden, - isEnabledByDefault: true); -} +using Microsoft.CodeAnalysis; + +namespace Esolang.Funge.Generator; + +/// +/// Provides diagnostic definitions reported during source generation. +/// +public static class DiagnosticDescriptors +{ + const string Category = "Funge"; + + /// + /// FG0001: Invalid source path parameter. + /// + public static readonly DiagnosticDescriptor InvalidSourcePathParameter = new( + id: "FG0001", + title: "Invalid source path parameter", + messageFormat: "The source path parameter of the attribute on the method '{0}' must not be null or empty", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// FG0002: Unsupported return type. + /// + public static readonly DiagnosticDescriptor InvalidReturnType = new( + id: "FG0002", + title: "Unsupported return type", + messageFormat: "The method return type '{0}' is not supported for Funge code generation", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// FG0003: Unsupported parameter type. + /// + public static readonly DiagnosticDescriptor InvalidParameter = new( + id: "FG0003", + title: "Unsupported parameter type", + messageFormat: "The parameter '{0}' of the method has an unsupported type", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// FG0004: Source file not found. + /// + public static readonly DiagnosticDescriptor SourceFileNotFound = new( + id: "FG0004", + title: "Funge source file not found", + messageFormat: "The Funge source file '{0}' could not be found in AdditionalFiles", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// FG0005: Consumer language version is below C# 8.0. + /// + public static readonly DiagnosticDescriptor LanguageVersionTooLow = new( + id: "FG0005", + title: "Language version too low", + messageFormat: "Funge source generation requires C# 8.0 or later (current: {0})", + category: Category, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + /// FG0006: Duplicate parameter type. + /// + public static readonly DiagnosticDescriptor DuplicateParameter = new( + id: "FG0006", + title: "Duplicate parameter type", + messageFormat: "The parameter type '{0}' appears more than once in method '{1}'", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// FG0007: Return type and output parameter conflict. + /// + public static readonly DiagnosticDescriptor ReturnOutputConflict = new( + id: "FG0007", + title: "Return type and output parameter conflict", + messageFormat: "Method '{0}' has both a non-void return type and an output parameter (TextWriter/PipeWriter); use one or the other", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// FG0008: Output interface required. + /// + public static readonly DiagnosticDescriptor RequiredOutputInterface = new( + id: "FG0008", + title: "Output interface required", + messageFormat: "Method '{0}' uses Funge output instructions but has no output (return string/IEnumerable<byte> or a TextWriter/PipeWriter parameter)", + category: Category, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); + + /// + /// FG0009: Input interface required. + /// + public static readonly DiagnosticDescriptor RequiredInputInterface = new( + id: "FG0009", + title: "Input interface required", + messageFormat: "Method '{0}' uses Funge input instructions but has no input (string, TextReader, or PipeReader parameter)", + category: Category, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); + + /// + /// FG0010: Unused input interface. + /// + public static readonly DiagnosticDescriptor UnusedInputInterface = new( + id: "FG0010", + title: "Unused input interface", + messageFormat: "Method '{0}' has an input parameter but the Funge source does not use any input instructions", + category: Category, + defaultSeverity: DiagnosticSeverity.Hidden, + isEnabledByDefault: true); +} diff --git a/Interpreter.Tests/ProgramTests.cs b/Interpreter.Tests/ProgramTests.cs index cdadf4f..22cf736 100644 --- a/Interpreter.Tests/ProgramTests.cs +++ b/Interpreter.Tests/ProgramTests.cs @@ -1,56 +1,56 @@ -using Esolang.Funge.Interpreter; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Esolang.Funge.Interpreter.Tests; - -[TestClass] -public class ProgramTests -{ - const string HelloWorldProgram = "64+\"!dlroW ,olleH\">:#,_@"; - - [TestMethod] - public async Task RunAsync_HelpOption_ReturnsZero() - { - var exitCode = await Program.RunAsync(["--help"]); - 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); - } - } -} +using Esolang.Funge.Interpreter; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Esolang.Funge.Interpreter.Tests; + +[TestClass] +public class ProgramTests +{ + const string HelloWorldProgram = "64+\"!dlroW ,olleH\">:#,_@"; + + [TestMethod] + public async Task RunAsync_HelpOption_ReturnsZero() + { + var exitCode = await Program.RunAsync(["--help"]); + 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/FungeInterpreterExtensions.cs b/Interpreter/FungeInterpreterExtensions.cs index 12c14bd..830eea3 100644 --- a/Interpreter/FungeInterpreterExtensions.cs +++ b/Interpreter/FungeInterpreterExtensions.cs @@ -1,7 +1,7 @@ using Esolang.Funge.Parser; using Esolang.Funge.Processor; -using System.CommandLine; using System.Collections; +using System.CommandLine; namespace Esolang.Funge.Interpreter; diff --git a/Interpreter/Program.cs b/Interpreter/Program.cs index b530114..4c0934d 100644 --- a/Interpreter/Program.cs +++ b/Interpreter/Program.cs @@ -1,40 +1,40 @@ -namespace Esolang.Funge.Interpreter; - -/// -/// Entry point for the dotnet-funge command-line tool. -/// -public static 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(); - 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; - } - } -} +namespace Esolang.Funge.Interpreter; + +/// +/// Entry point for the dotnet-funge command-line tool. +/// +public static 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(); + 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; + } + } +} diff --git a/Parser.Tests/FungeParserTests.cs b/Parser.Tests/FungeParserTests.cs index 9e75439..5e27f98 100644 --- a/Parser.Tests/FungeParserTests.cs +++ b/Parser.Tests/FungeParserTests.cs @@ -1,167 +1,167 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Esolang.Funge.Parser.Tests; - -[TestClass] -public class FungeParserTests -{ - [TestMethod] - public void ParseSingleChar_StoresCorrectly() - { - var space = FungeParser.Parse("@"); - Assert.AreEqual('@', space[new FungeVector(0, 0)]); - } - - [TestMethod] - public void ParseSpace_ReturnsDefaultCell() - { - var space = FungeParser.Parse(" @"); - Assert.AreEqual(' ', space[new FungeVector(0, 0)]); - Assert.AreEqual('@', space[new FungeVector(1, 0)]); - } - - [TestMethod] - public void ParseMultiLine_CorrectCoordinates() - { - var source = "AB\nCD"; - var space = FungeParser.Parse(source); - Assert.AreEqual('A', space[new FungeVector(0, 0)]); - Assert.AreEqual('B', space[new FungeVector(1, 0)]); - Assert.AreEqual('C', space[new FungeVector(0, 1)]); - Assert.AreEqual('D', space[new FungeVector(1, 1)]); - } - - [TestMethod] - public void ParseCrLf_IgnoresCarriageReturn() - { - var space = FungeParser.Parse("A\r\nB"); - Assert.AreEqual('A', space[new FungeVector(0, 0)]); - Assert.AreEqual('B', space[new FungeVector(0, 1)]); - } - - [TestMethod] - public void UnsetCell_ReturnsSpace() - { - var space = FungeParser.Parse("@"); - Assert.AreEqual(' ', space[new FungeVector(99, 99)]); - } - - [TestMethod] - public void BoundingBox_CorrectAfterParse() - { - var space = FungeParser.Parse("AB\nCD"); - Assert.AreEqual(0, space.MinX); - Assert.AreEqual(0, space.MinY); - Assert.AreEqual(0, space.MinZ); - Assert.AreEqual(1, space.MaxX); - Assert.AreEqual(1, space.MaxY); - Assert.AreEqual(0, space.MaxZ); - } - - [TestMethod] - public void BoundingBox_IncludesSpacesInSource() - { - var space = FungeParser.Parse("A "); - Assert.AreEqual(0, space.MinX); - Assert.AreEqual(2, space.MaxX); - Assert.AreEqual(0, space.MinY); - Assert.AreEqual(0, space.MaxY); - Assert.AreEqual(0, space.MinZ); - Assert.AreEqual(0, space.MaxZ); - } - - [TestMethod] - public void Parse_SgmlSpaces_AreTreatedAsSpaceCells() - { - var space = FungeParser.Parse("A\t\vB"); - Assert.AreEqual('A', space[new FungeVector(0, 0)]); - Assert.AreEqual(' ', space[new FungeVector(1, 0)]); - Assert.AreEqual(' ', space[new FungeVector(2, 0)]); - Assert.AreEqual('B', space[new FungeVector(3, 0)]); - } - - [TestMethod] - public void Parse_FormFeed_StartsNewLayer() - { - var space = FungeParser.Parse("A\fB"); - Assert.AreEqual('A', space[new FungeVector(0, 0, 0)]); - Assert.AreEqual('B', space[new FungeVector(0, 0, 1)]); - Assert.AreEqual(0, space.MinZ); - Assert.AreEqual(1, space.MaxZ); - } -} - -[TestClass] -public class FungeVectorTests -{ - [TestMethod] - public void RotateRight_EastBecomeSouth() - => Assert.AreEqual(FungeVector.South, FungeVector.East.RotateRight()); - - [TestMethod] - public void RotateRight_SouthBecomeWest() - => Assert.AreEqual(FungeVector.West, FungeVector.South.RotateRight()); - - [TestMethod] - public void RotateLeft_EastBecomeNorth() - => Assert.AreEqual(FungeVector.North, FungeVector.East.RotateLeft()); - - [TestMethod] - public void Reflect_EastBecomeWest() - => Assert.AreEqual(FungeVector.West, FungeVector.East.Reflect()); - - [TestMethod] - public void Addition() - => Assert.AreEqual(new FungeVector(3, 5, 7), new FungeVector(1, 2, 3) + new FungeVector(2, 3, 4)); -} - -[TestClass] -public class FungeSpaceTests -{ - [TestMethod] - public void Advance_WrapsEastBeyondMaxX() - { - var space = FungeParser.Parse("ABC"); - // MinX=0, MaxX=2, Width=3 - // Advance East from (2,0): next (3,0) -> wraps to (0,0) - var next = space.Advance(new FungeVector(2, 0), FungeVector.East); - Assert.AreEqual(new FungeVector(0, 0), next); - } - - [TestMethod] - public void Advance_WrapsWestBeyondMinX() - { - var space = FungeParser.Parse("ABC"); - var next = space.Advance(new FungeVector(0, 0), FungeVector.West); - Assert.AreEqual(new FungeVector(2, 0), next); - } - - [TestMethod] - public void Advance_WrapsSouthBeyondMaxY() - { - var space = FungeParser.Parse("A\nB\nC"); - var next = space.Advance(new FungeVector(0, 2), FungeVector.South); - Assert.AreEqual(new FungeVector(0, 0), next); - } - - [TestMethod] - public void SetCell_UpdatesBoundingBox() - { - var space = new FungeSpace(); - space[new FungeVector(5, 10, 15)] = 'X'; - Assert.AreEqual(5, space.MinX); - Assert.AreEqual(5, space.MaxX); - Assert.AreEqual(10, space.MinY); - Assert.AreEqual(10, space.MaxY); - Assert.AreEqual(15, space.MinZ); - Assert.AreEqual(15, space.MaxZ); - } - - [TestMethod] - public void Advance_WrapsLowBeyondMaxZ() - { - var space = FungeParser.Parse("A\fB"); - var next = space.Advance(new FungeVector(0, 0, 1), FungeVector.Low); - Assert.AreEqual(new FungeVector(0, 0, 0), next); - } -} +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Esolang.Funge.Parser.Tests; + +[TestClass] +public class FungeParserTests +{ + [TestMethod] + public void ParseSingleChar_StoresCorrectly() + { + var space = FungeParser.Parse("@"); + Assert.AreEqual('@', space[new FungeVector(0, 0)]); + } + + [TestMethod] + public void ParseSpace_ReturnsDefaultCell() + { + var space = FungeParser.Parse(" @"); + Assert.AreEqual(' ', space[new FungeVector(0, 0)]); + Assert.AreEqual('@', space[new FungeVector(1, 0)]); + } + + [TestMethod] + public void ParseMultiLine_CorrectCoordinates() + { + var source = "AB\nCD"; + var space = FungeParser.Parse(source); + Assert.AreEqual('A', space[new FungeVector(0, 0)]); + Assert.AreEqual('B', space[new FungeVector(1, 0)]); + Assert.AreEqual('C', space[new FungeVector(0, 1)]); + Assert.AreEqual('D', space[new FungeVector(1, 1)]); + } + + [TestMethod] + public void ParseCrLf_IgnoresCarriageReturn() + { + var space = FungeParser.Parse("A\r\nB"); + Assert.AreEqual('A', space[new FungeVector(0, 0)]); + Assert.AreEqual('B', space[new FungeVector(0, 1)]); + } + + [TestMethod] + public void UnsetCell_ReturnsSpace() + { + var space = FungeParser.Parse("@"); + Assert.AreEqual(' ', space[new FungeVector(99, 99)]); + } + + [TestMethod] + public void BoundingBox_CorrectAfterParse() + { + var space = FungeParser.Parse("AB\nCD"); + Assert.AreEqual(0, space.MinX); + Assert.AreEqual(0, space.MinY); + Assert.AreEqual(0, space.MinZ); + Assert.AreEqual(1, space.MaxX); + Assert.AreEqual(1, space.MaxY); + Assert.AreEqual(0, space.MaxZ); + } + + [TestMethod] + public void BoundingBox_IncludesSpacesInSource() + { + var space = FungeParser.Parse("A "); + Assert.AreEqual(0, space.MinX); + Assert.AreEqual(2, space.MaxX); + Assert.AreEqual(0, space.MinY); + Assert.AreEqual(0, space.MaxY); + Assert.AreEqual(0, space.MinZ); + Assert.AreEqual(0, space.MaxZ); + } + + [TestMethod] + public void Parse_SgmlSpaces_AreTreatedAsSpaceCells() + { + var space = FungeParser.Parse("A\t\vB"); + Assert.AreEqual('A', space[new FungeVector(0, 0)]); + Assert.AreEqual(' ', space[new FungeVector(1, 0)]); + Assert.AreEqual(' ', space[new FungeVector(2, 0)]); + Assert.AreEqual('B', space[new FungeVector(3, 0)]); + } + + [TestMethod] + public void Parse_FormFeed_StartsNewLayer() + { + var space = FungeParser.Parse("A\fB"); + Assert.AreEqual('A', space[new FungeVector(0, 0, 0)]); + Assert.AreEqual('B', space[new FungeVector(0, 0, 1)]); + Assert.AreEqual(0, space.MinZ); + Assert.AreEqual(1, space.MaxZ); + } +} + +[TestClass] +public class FungeVectorTests +{ + [TestMethod] + public void RotateRight_EastBecomeSouth() + => Assert.AreEqual(FungeVector.South, FungeVector.East.RotateRight()); + + [TestMethod] + public void RotateRight_SouthBecomeWest() + => Assert.AreEqual(FungeVector.West, FungeVector.South.RotateRight()); + + [TestMethod] + public void RotateLeft_EastBecomeNorth() + => Assert.AreEqual(FungeVector.North, FungeVector.East.RotateLeft()); + + [TestMethod] + public void Reflect_EastBecomeWest() + => Assert.AreEqual(FungeVector.West, FungeVector.East.Reflect()); + + [TestMethod] + public void Addition() + => Assert.AreEqual(new FungeVector(3, 5, 7), new FungeVector(1, 2, 3) + new FungeVector(2, 3, 4)); +} + +[TestClass] +public class FungeSpaceTests +{ + [TestMethod] + public void Advance_WrapsEastBeyondMaxX() + { + var space = FungeParser.Parse("ABC"); + // MinX=0, MaxX=2, Width=3 + // Advance East from (2,0): next (3,0) -> wraps to (0,0) + var next = space.Advance(new FungeVector(2, 0), FungeVector.East); + Assert.AreEqual(new FungeVector(0, 0), next); + } + + [TestMethod] + public void Advance_WrapsWestBeyondMinX() + { + var space = FungeParser.Parse("ABC"); + var next = space.Advance(new FungeVector(0, 0), FungeVector.West); + Assert.AreEqual(new FungeVector(2, 0), next); + } + + [TestMethod] + public void Advance_WrapsSouthBeyondMaxY() + { + var space = FungeParser.Parse("A\nB\nC"); + var next = space.Advance(new FungeVector(0, 2), FungeVector.South); + Assert.AreEqual(new FungeVector(0, 0), next); + } + + [TestMethod] + public void SetCell_UpdatesBoundingBox() + { + var space = new FungeSpace(); + space[new FungeVector(5, 10, 15)] = 'X'; + Assert.AreEqual(5, space.MinX); + Assert.AreEqual(5, space.MaxX); + Assert.AreEqual(10, space.MinY); + Assert.AreEqual(10, space.MaxY); + Assert.AreEqual(15, space.MinZ); + Assert.AreEqual(15, space.MaxZ); + } + + [TestMethod] + public void Advance_WrapsLowBeyondMaxZ() + { + var space = FungeParser.Parse("A\fB"); + var next = space.Advance(new FungeVector(0, 0, 1), FungeVector.Low); + Assert.AreEqual(new FungeVector(0, 0, 0), next); + } +} diff --git a/Parser/FungeParser.cs b/Parser/FungeParser.cs index 37077e3..3c9000f 100644 --- a/Parser/FungeParser.cs +++ b/Parser/FungeParser.cs @@ -1,46 +1,46 @@ -namespace Esolang.Funge.Parser; - -/// -/// Parses Funge-98 source text into a . -/// -public static class FungeParser -{ - /// - /// Parses a Funge-98 source string into a populated . - /// Each character is placed at its (column, row) coordinate. - /// Space characters (ASCII 32) are not stored; they use the default cell value. - /// - /// The Funge-98 source text. - /// A containing the program. - public static FungeSpace Parse(string source) - { - var space = new FungeSpace(); - int x = 0, y = 0, z = 0; - foreach (var ch in source) - { - if (ch == '\r') continue; - if (ch == '\n') { x = 0; y++; continue; } - if (ch == '\f') { x = 0; y = 0; z++; continue; } - - var cell = ch switch - { - '\t' or '\v' => ' ', - _ => ch, - }; - - var pos = new FungeVector(x, y, z); - space.EnsureBounds(pos); - if (cell != ' ') - space[pos] = cell; - x++; - } - return space; - } - - /// - /// Reads a file and parses its contents as a Funge-98 program. - /// - /// Path to the source file. - /// A containing the program. - public static FungeSpace ParseFile(string path) => Parse(File.ReadAllText(path)); -} +namespace Esolang.Funge.Parser; + +/// +/// Parses Funge-98 source text into a . +/// +public static class FungeParser +{ + /// + /// Parses a Funge-98 source string into a populated . + /// Each character is placed at its (column, row) coordinate. + /// Space characters (ASCII 32) are not stored; they use the default cell value. + /// + /// The Funge-98 source text. + /// A containing the program. + public static FungeSpace Parse(string source) + { + var space = new FungeSpace(); + int x = 0, y = 0, z = 0; + foreach (var ch in source) + { + if (ch == '\r') continue; + if (ch == '\n') { x = 0; y++; continue; } + if (ch == '\f') { x = 0; y = 0; z++; continue; } + + var cell = ch switch + { + '\t' or '\v' => ' ', + _ => ch, + }; + + var pos = new FungeVector(x, y, z); + space.EnsureBounds(pos); + if (cell != ' ') + space[pos] = cell; + x++; + } + return space; + } + + /// + /// Reads a file and parses its contents as a Funge-98 program. + /// + /// Path to the source file. + /// A containing the program. + public static FungeSpace ParseFile(string path) => Parse(File.ReadAllText(path)); +} diff --git a/Parser/FungeSpace.cs b/Parser/FungeSpace.cs index 894b19a..11024a3 100644 --- a/Parser/FungeSpace.cs +++ b/Parser/FungeSpace.cs @@ -1,115 +1,115 @@ -namespace Esolang.Funge.Parser; - -/// -/// Represents the Funge-98 program space: a sparse, conceptually infinite 3D grid of integer cells. -/// Unset cells default to the space character (ASCII 32). -/// -public sealed class FungeSpace -{ - private readonly Dictionary _cells = new(); - private int _minX, _minY, _minZ, _maxX, _maxY, _maxZ; - private bool _hasAny; - - private void IncludeInBounds(FungeVector pos) - { - if (!_hasAny) - { - _minX = _maxX = pos.X; - _minY = _maxY = pos.Y; - _minZ = _maxZ = pos.Z; - _hasAny = true; - return; - } - - if (pos.X < _minX) _minX = pos.X; - if (pos.X > _maxX) _maxX = pos.X; - if (pos.Y < _minY) _minY = pos.Y; - if (pos.Y > _maxY) _maxY = pos.Y; - if (pos.Z < _minZ) _minZ = pos.Z; - if (pos.Z > _maxZ) _maxZ = pos.Z; - } - - /// - /// Gets or sets the integer value at the given position. - /// Unset positions return ' ' (32). - /// Setting a cell to ' ' removes it from the space. - /// - public int this[FungeVector pos] - { - get => _cells.TryGetValue(pos, out var v) ? v : ' '; - set - { - if (value == ' ') - { - _cells.Remove(pos); - } - else - { - _cells[pos] = value; - IncludeInBounds(pos); - } - } - } - - /// - /// Ensures the given position is included in the Least Significant Bounding Box - /// even when the cell value is a space and therefore not explicitly stored. - /// - public void EnsureBounds(FungeVector pos) => IncludeInBounds(pos); - - /// Minimum X coordinate of the populated bounding box. - public int MinX => _minX; - - /// Minimum Y coordinate of the populated bounding box. - public int MinY => _minY; - - /// Maximum X coordinate of the populated bounding box. - public int MaxX => _maxX; - - /// Maximum Y coordinate of the populated bounding box. - public int MaxY => _maxY; - - /// Minimum Z coordinate of the populated bounding box. - public int MinZ => _minZ; - - /// Maximum Z coordinate of the populated bounding box. - public int MaxZ => _maxZ; - - /// - /// Advances a position by , wrapping around the Least Significant - /// Bounding Box (LSAB) when the result would leave it. - /// - /// Current position. - /// Movement delta. - /// The next position after wrapping. - public FungeVector Advance(FungeVector pos, FungeVector delta) - { - if (!_hasAny) - return pos; - - var nextX = pos.X + delta.X; - var nextY = pos.Y + delta.Y; - var nextZ = pos.Z + delta.Z; - - var width = _maxX - _minX + 1; - var height = _maxY - _minY + 1; - var depth = _maxZ - _minZ + 1; - - if (nextX < _minX) - nextX = _maxX - ((_minX - nextX - 1) % width); - else if (nextX > _maxX) - nextX = _minX + ((nextX - _maxX - 1) % width); - - if (nextY < _minY) - nextY = _maxY - ((_minY - nextY - 1) % height); - else if (nextY > _maxY) - nextY = _minY + ((nextY - _maxY - 1) % height); - - if (nextZ < _minZ) - nextZ = _maxZ - ((_minZ - nextZ - 1) % depth); - else if (nextZ > _maxZ) - nextZ = _minZ + ((nextZ - _maxZ - 1) % depth); - - return new FungeVector(nextX, nextY, nextZ); - } -} +namespace Esolang.Funge.Parser; + +/// +/// Represents the Funge-98 program space: a sparse, conceptually infinite 3D grid of integer cells. +/// Unset cells default to the space character (ASCII 32). +/// +public sealed class FungeSpace +{ + private readonly Dictionary _cells = new(); + private int _minX, _minY, _minZ, _maxX, _maxY, _maxZ; + private bool _hasAny; + + private void IncludeInBounds(FungeVector pos) + { + if (!_hasAny) + { + _minX = _maxX = pos.X; + _minY = _maxY = pos.Y; + _minZ = _maxZ = pos.Z; + _hasAny = true; + return; + } + + if (pos.X < _minX) _minX = pos.X; + if (pos.X > _maxX) _maxX = pos.X; + if (pos.Y < _minY) _minY = pos.Y; + if (pos.Y > _maxY) _maxY = pos.Y; + if (pos.Z < _minZ) _minZ = pos.Z; + if (pos.Z > _maxZ) _maxZ = pos.Z; + } + + /// + /// Gets or sets the integer value at the given position. + /// Unset positions return ' ' (32). + /// Setting a cell to ' ' removes it from the space. + /// + public int this[FungeVector pos] + { + get => _cells.TryGetValue(pos, out var v) ? v : ' '; + set + { + if (value == ' ') + { + _cells.Remove(pos); + } + else + { + _cells[pos] = value; + IncludeInBounds(pos); + } + } + } + + /// + /// Ensures the given position is included in the Least Significant Bounding Box + /// even when the cell value is a space and therefore not explicitly stored. + /// + public void EnsureBounds(FungeVector pos) => IncludeInBounds(pos); + + /// Minimum X coordinate of the populated bounding box. + public int MinX => _minX; + + /// Minimum Y coordinate of the populated bounding box. + public int MinY => _minY; + + /// Maximum X coordinate of the populated bounding box. + public int MaxX => _maxX; + + /// Maximum Y coordinate of the populated bounding box. + public int MaxY => _maxY; + + /// Minimum Z coordinate of the populated bounding box. + public int MinZ => _minZ; + + /// Maximum Z coordinate of the populated bounding box. + public int MaxZ => _maxZ; + + /// + /// Advances a position by , wrapping around the Least Significant + /// Bounding Box (LSAB) when the result would leave it. + /// + /// Current position. + /// Movement delta. + /// The next position after wrapping. + public FungeVector Advance(FungeVector pos, FungeVector delta) + { + if (!_hasAny) + return pos; + + var nextX = pos.X + delta.X; + var nextY = pos.Y + delta.Y; + var nextZ = pos.Z + delta.Z; + + var width = _maxX - _minX + 1; + var height = _maxY - _minY + 1; + var depth = _maxZ - _minZ + 1; + + if (nextX < _minX) + nextX = _maxX - ((_minX - nextX - 1) % width); + else if (nextX > _maxX) + nextX = _minX + ((nextX - _maxX - 1) % width); + + if (nextY < _minY) + nextY = _maxY - ((_minY - nextY - 1) % height); + else if (nextY > _maxY) + nextY = _minY + ((nextY - _maxY - 1) % height); + + if (nextZ < _minZ) + nextZ = _maxZ - ((_minZ - nextZ - 1) % depth); + else if (nextZ > _maxZ) + nextZ = _minZ + ((nextZ - _maxZ - 1) % depth); + + return new FungeVector(nextX, nextY, nextZ); + } +} diff --git a/Parser/FungeVector.cs b/Parser/FungeVector.cs index e5020bc..a80fc88 100644 --- a/Parser/FungeVector.cs +++ b/Parser/FungeVector.cs @@ -1,90 +1,90 @@ -namespace Esolang.Funge.Parser; - -/// -/// Represents a 3D integer vector used for positions and deltas in Funge-98. -/// -public readonly struct FungeVector : IEquatable -{ - /// The X component. - public int X { get; } - - /// The Y component. - public int Y { get; } - - /// The Z component. - public int Z { get; } - - /// Initializes a new with the given components. - public FungeVector(int x, int y, int z) - { - X = x; - Y = y; - Z = z; - } - - /// Initializes a new in 2D (Z=0). - public FungeVector(int x, int y) : this(x, y, 0) { } - - /// - public bool Equals(FungeVector other) => X == other.X && Y == other.Y && Z == other.Z; - - /// - public override bool Equals(object? obj) => obj is FungeVector v && Equals(v); - - /// - public override int GetHashCode() => HashCode.Combine(HashCode.Combine(X, Y), Z); - - /// Equality operator. - public static bool operator ==(FungeVector left, FungeVector right) => left.Equals(right); - - /// Inequality operator. - public static bool operator !=(FungeVector left, FungeVector right) => !left.Equals(right); - - /// - public override string ToString() => $"({X}, {Y}, {Z})"; - - /// Delta for East direction (right): (1, 0). - public static readonly FungeVector East = new(1, 0); - - /// Delta for West direction (left): (-1, 0). - public static readonly FungeVector West = new(-1, 0); - - /// Delta for North direction (up): (0, -1). - public static readonly FungeVector North = new(0, -1); - - /// Delta for South direction (down): (0, 1). - public static readonly FungeVector South = new(0, 1); - - /// Delta for High direction (towards -Z): (0, 0, -1). - public static readonly FungeVector High = new(0, 0, -1); - - /// Delta for Low direction (towards +Z): (0, 0, 1). - public static readonly FungeVector Low = new(0, 0, 1); - - /// Adds two vectors. - public static FungeVector operator +(FungeVector a, FungeVector b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); - - /// Subtracts two vectors. - public static FungeVector operator -(FungeVector a, FungeVector b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z); - - /// Negates a vector. - public static FungeVector operator -(FungeVector a) => new(-a.X, -a.Y, -a.Z); - - /// Scales a vector by a scalar. - public static FungeVector operator *(FungeVector a, int scalar) => new(a.X * scalar, a.Y * scalar, a.Z * scalar); - - /// - /// Rotates 90 degrees clockwise (Turn Right ]). - /// - public FungeVector RotateRight() => new(-Y, X, Z); - - /// - /// Rotates 90 degrees counter-clockwise (Turn Left [). - /// - public FungeVector RotateLeft() => new(Y, -X, Z); - - /// - /// Reflects the vector, reversing direction (r). - /// - public FungeVector Reflect() => new(-X, -Y, -Z); -} +namespace Esolang.Funge.Parser; + +/// +/// Represents a 3D integer vector used for positions and deltas in Funge-98. +/// +public readonly struct FungeVector : IEquatable +{ + /// The X component. + public int X { get; } + + /// The Y component. + public int Y { get; } + + /// The Z component. + public int Z { get; } + + /// Initializes a new with the given components. + public FungeVector(int x, int y, int z) + { + X = x; + Y = y; + Z = z; + } + + /// Initializes a new in 2D (Z=0). + public FungeVector(int x, int y) : this(x, y, 0) { } + + /// + public bool Equals(FungeVector other) => X == other.X && Y == other.Y && Z == other.Z; + + /// + public override bool Equals(object? obj) => obj is FungeVector v && Equals(v); + + /// + public override int GetHashCode() => HashCode.Combine(HashCode.Combine(X, Y), Z); + + /// Equality operator. + public static bool operator ==(FungeVector left, FungeVector right) => left.Equals(right); + + /// Inequality operator. + public static bool operator !=(FungeVector left, FungeVector right) => !left.Equals(right); + + /// + public override string ToString() => $"({X}, {Y}, {Z})"; + + /// Delta for East direction (right): (1, 0). + public static readonly FungeVector East = new(1, 0); + + /// Delta for West direction (left): (-1, 0). + public static readonly FungeVector West = new(-1, 0); + + /// Delta for North direction (up): (0, -1). + public static readonly FungeVector North = new(0, -1); + + /// Delta for South direction (down): (0, 1). + public static readonly FungeVector South = new(0, 1); + + /// Delta for High direction (towards -Z): (0, 0, -1). + public static readonly FungeVector High = new(0, 0, -1); + + /// Delta for Low direction (towards +Z): (0, 0, 1). + public static readonly FungeVector Low = new(0, 0, 1); + + /// Adds two vectors. + public static FungeVector operator +(FungeVector a, FungeVector b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); + + /// Subtracts two vectors. + public static FungeVector operator -(FungeVector a, FungeVector b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z); + + /// Negates a vector. + public static FungeVector operator -(FungeVector a) => new(-a.X, -a.Y, -a.Z); + + /// Scales a vector by a scalar. + public static FungeVector operator *(FungeVector a, int scalar) => new(a.X * scalar, a.Y * scalar, a.Z * scalar); + + /// + /// Rotates 90 degrees clockwise (Turn Right ]). + /// + public FungeVector RotateRight() => new(-Y, X, Z); + + /// + /// Rotates 90 degrees counter-clockwise (Turn Left [). + /// + public FungeVector RotateLeft() => new(Y, -X, Z); + + /// + /// Reflects the vector, reversing direction (r). + /// + public FungeVector Reflect() => new(-X, -Y, -Z); +} diff --git a/Parser/Shared/HashCode.cs b/Parser/Shared/HashCode.cs index 82827ce..96fdf12 100644 --- a/Parser/Shared/HashCode.cs +++ b/Parser/Shared/HashCode.cs @@ -1,15 +1,15 @@ -#if !NETSTANDARD2_1_OR_GREATER && !NET5_0_OR_GREATER -// Minimal HashCode polyfill for netstandard2.0 -namespace System; - -internal static class HashCode -{ - public static int Combine(T1 v1, T2 v2) - { - var h1 = v1?.GetHashCode() ?? 0; - var h2 = v2?.GetHashCode() ?? 0; - var rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27); - return ((int)rol5 + h1) ^ h2; - } -} -#endif +#if !NETSTANDARD2_1_OR_GREATER && !NET5_0_OR_GREATER +// Minimal HashCode polyfill for netstandard2.0 +namespace System; + +internal static class HashCode +{ + public static int Combine(T1 v1, T2 v2) + { + var h1 = v1?.GetHashCode() ?? 0; + var h2 = v2?.GetHashCode() ?? 0; + var rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27); + return ((int)rol5 + h1) ^ h2; + } +} +#endif diff --git a/Parser/Shared/IsExternalInit.cs b/Parser/Shared/IsExternalInit.cs index bf98ee8..1330bba 100644 --- a/Parser/Shared/IsExternalInit.cs +++ b/Parser/Shared/IsExternalInit.cs @@ -1,8 +1,8 @@ -#if !NET5_0_OR_GREATER -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Runtime.CompilerServices; - -internal sealed class IsExternalInit { } -#endif +#if !NET5_0_OR_GREATER +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +internal sealed class IsExternalInit { } +#endif diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index 80d9463..74dfd2f 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -1,7 +1,7 @@ using Esolang.Funge.Parser; using Esolang.Processor; -using System.Diagnostics; using System.Collections; +using System.Diagnostics; namespace Esolang.Funge.Processor; diff --git a/Processor/InstructionPointer.cs b/Processor/InstructionPointer.cs index 1908353..3dec861 100644 --- a/Processor/InstructionPointer.cs +++ b/Processor/InstructionPointer.cs @@ -1,54 +1,54 @@ -using Esolang.Funge.Parser; - -namespace Esolang.Funge.Processor; - -/// -/// Represents the execution state of a single Instruction Pointer (IP) in Funge-98. -/// -public sealed class InstructionPointer -{ - /// Gets the unique identifier for this IP. - public int Id { get; } - - /// Gets or sets the current position in FungeSpace. - public FungeVector Position { get; set; } - - /// Gets or sets the current movement delta (direction). - public FungeVector Delta { get; set; } = FungeVector.East; - - /// Gets or sets the storage offset used by g/p instructions. - public FungeVector Offset { get; set; } - - /// Gets the stack stack for this IP. - public StackStack StackStack { get; } - - /// Gets or sets whether this IP is in string mode. - public bool StringMode { get; set; } - - /// Gets or sets whether this IP has been stopped (by @). - public bool IsStopped { get; set; } - - /// 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) - { - Id = id; - StackStack = stackStack; - } - - /// - /// Creates a child IP for the t (Split) instruction. - /// The child shares the same position, a deep copy of the stack stack, - /// and a reflected delta. - /// - /// Unique ID for the new child IP. - /// The new child IP. - public InstructionPointer CreateChild(int newId) => new(newId, StackStack.Clone()) - { - Position = Position, - Delta = Delta.Reflect(), - Offset = Offset, - StringMode = StringMode, - }; -} +using Esolang.Funge.Parser; + +namespace Esolang.Funge.Processor; + +/// +/// Represents the execution state of a single Instruction Pointer (IP) in Funge-98. +/// +public sealed class InstructionPointer +{ + /// Gets the unique identifier for this IP. + public int Id { get; } + + /// Gets or sets the current position in FungeSpace. + public FungeVector Position { get; set; } + + /// Gets or sets the current movement delta (direction). + public FungeVector Delta { get; set; } = FungeVector.East; + + /// Gets or sets the storage offset used by g/p instructions. + public FungeVector Offset { get; set; } + + /// Gets the stack stack for this IP. + public StackStack StackStack { get; } + + /// Gets or sets whether this IP is in string mode. + public bool StringMode { get; set; } + + /// Gets or sets whether this IP has been stopped (by @). + public bool IsStopped { get; set; } + + /// 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) + { + Id = id; + StackStack = stackStack; + } + + /// + /// Creates a child IP for the t (Split) instruction. + /// The child shares the same position, a deep copy of the stack stack, + /// and a reflected delta. + /// + /// Unique ID for the new child IP. + /// The new child IP. + public InstructionPointer CreateChild(int newId) => new(newId, StackStack.Clone()) + { + Position = Position, + Delta = Delta.Reflect(), + Offset = Offset, + StringMode = StringMode, + }; +} diff --git a/Processor/StackStack.cs b/Processor/StackStack.cs index 5f67a20..d101cde 100644 --- a/Processor/StackStack.cs +++ b/Processor/StackStack.cs @@ -1,65 +1,65 @@ -namespace Esolang.Funge.Processor; - -/// -/// Implements the Funge-98 stack stack: a stack of stacks. -/// The topmost stack is the TOSS (Top Of Stack Stack). -/// The second stack (if present) is the SOSS (Second On Stack Stack). -/// -public sealed class StackStack -{ - private 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; - - /// Gets the top-of-stack-stack (current active stack). - public Stack TOSS => _stacks.First!.Value; - - /// Gets the second-on-stack-stack, or if there is only one stack. - public Stack? SOSS => _stacks.Count >= 2 ? _stacks.First!.Next!.Value : null; - - /// Gets whether a SOSS exists. - public bool HasSOSS => _stacks.Count >= 2; - - /// Gets the total number of stacks in the stack stack. - public int StackCount => _stacks.Count; - - /// Pushes a value onto the TOSS. - public void Push(int value) => TOSS.Push(value); - - /// Pops a value from the TOSS. Returns 0 if the TOSS is empty. - public int Pop() => TOSS.Count > 0 ? TOSS.Pop() : 0; - - /// Peeks at the top of the TOSS without removing it. Returns 0 if empty. - public int Peek() => TOSS.Count > 0 ? TOSS.Peek() : 0; - - /// Pushes a new empty stack onto the stack stack, making it the new TOSS. - public void PushNewStack() => _stacks.AddFirst(new Stack()); - - /// - /// Pops the current TOSS from the stack stack (discarding its remaining contents), - /// making the previous SOSS the new TOSS. Does nothing if there is only one stack. - /// - public void PopCurrentStack() - { - if (_stacks.Count > 1) - _stacks.RemoveFirst(); - } - - /// Clears all items from the TOSS. - public void ClearToss() => TOSS.Clear(); - - /// Enumerates all stacks from TOSS downward. - public IEnumerable> AllStacks => _stacks; - - /// Creates a deep copy of this stack stack. - public StackStack Clone() - { - var newList = new LinkedList>(); - foreach (var stack in _stacks) - newList.AddLast(new Stack(stack.Reverse())); - return new StackStack(newList); - } -} +namespace Esolang.Funge.Processor; + +/// +/// Implements the Funge-98 stack stack: a stack of stacks. +/// The topmost stack is the TOSS (Top Of Stack Stack). +/// The second stack (if present) is the SOSS (Second On Stack Stack). +/// +public sealed class StackStack +{ + private 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; + + /// Gets the top-of-stack-stack (current active stack). + public Stack TOSS => _stacks.First!.Value; + + /// Gets the second-on-stack-stack, or if there is only one stack. + public Stack? SOSS => _stacks.Count >= 2 ? _stacks.First!.Next!.Value : null; + + /// Gets whether a SOSS exists. + public bool HasSOSS => _stacks.Count >= 2; + + /// Gets the total number of stacks in the stack stack. + public int StackCount => _stacks.Count; + + /// Pushes a value onto the TOSS. + public void Push(int value) => TOSS.Push(value); + + /// Pops a value from the TOSS. Returns 0 if the TOSS is empty. + public int Pop() => TOSS.Count > 0 ? TOSS.Pop() : 0; + + /// Peeks at the top of the TOSS without removing it. Returns 0 if empty. + public int Peek() => TOSS.Count > 0 ? TOSS.Peek() : 0; + + /// Pushes a new empty stack onto the stack stack, making it the new TOSS. + public void PushNewStack() => _stacks.AddFirst(new Stack()); + + /// + /// Pops the current TOSS from the stack stack (discarding its remaining contents), + /// making the previous SOSS the new TOSS. Does nothing if there is only one stack. + /// + public void PopCurrentStack() + { + if (_stacks.Count > 1) + _stacks.RemoveFirst(); + } + + /// Clears all items from the TOSS. + public void ClearToss() => TOSS.Clear(); + + /// Enumerates all stacks from TOSS downward. + public IEnumerable> AllStacks => _stacks; + + /// Creates a deep copy of this stack stack. + public StackStack Clone() + { + var newList = new LinkedList>(); + foreach (var stack in _stacks) + newList.AddLast(new Stack(stack.Reverse())); + return new StackStack(newList); + } +} diff --git a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs index 1b87058..6da1c86 100644 --- a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs +++ b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs @@ -1,62 +1,62 @@ -using Esolang.Funge; -using System.Text; - -// string return (file-based) -Console.WriteLine($"{nameof(FungeSample.HelloWorld)}: {FungeSample.HelloWorld()}"); - -// Task return (file-based) -Console.WriteLine($"{nameof(FungeSample.HelloWorldAsync)}: {await FungeSample.HelloWorldAsync()}"); - -// void with TextWriter output (file-based) -using var textWriter = new StringWriter(); -FungeSample.HelloWorldWriter(textWriter); -Console.WriteLine($"{nameof(FungeSample.HelloWorldWriter)}: {textWriter}"); - -// IEnumerable (file-based) -Console.WriteLine($"{nameof(FungeSample.HelloWorldBytes)}: {Encoding.UTF8.GetString(FungeSample.HelloWorldBytes().ToArray())}"); - -// IAsyncEnumerable (file-based) -Console.WriteLine($"{nameof(FungeSample.HelloWorldBytesAsync)}: {Encoding.UTF8.GetString(await ToByteArrayAsync(FungeSample.HelloWorldBytesAsync()))}"); - -// Inline source — same "Hello, World!" program embedded as a string literal -Console.WriteLine($"{nameof(FungeSample.HelloWorldInline)}: {FungeSample.HelloWorldInline()}"); - -// 3D (Trefunge) — layer Z=0 uses 'l' to jump into layer Z=1 where Hello World runs -Console.WriteLine($"{nameof(FungeSample.HelloWorld3D)}: {FungeSample.HelloWorld3D()}"); - -static async Task ToByteArrayAsync(IAsyncEnumerable source) -{ - var list = new List(); - await foreach (var b in source) - list.Add(b); - return [.. list]; -} - -namespace Esolang.Funge -{ - partial class FungeSample - { - [GenerateFungeMethod("Programs/hello.b98")] - public static partial string HelloWorld(); - - [GenerateFungeMethod("Programs/hello.b98")] - public static partial Task HelloWorldAsync(); - - [GenerateFungeMethod("Programs/hello.b98")] - public static partial void HelloWorldWriter(System.IO.TextWriter output); - - [GenerateFungeMethod("Programs/hello.b98")] - public static partial IEnumerable HelloWorldBytes(); - - [GenerateFungeMethod("Programs/hello.b98")] - public static partial IAsyncEnumerable HelloWorldBytesAsync(); - - // InlineSource: no .b98 file needed — Funge-98 code is embedded directly - [GenerateFungeMethod(InlineSource = "64+\"!dlroW ,olleH\">:#,_@")] - public static partial string HelloWorldInline(); - - // 3D (Trefunge): layer Z=0 executes 'l' (go low) to enter layer Z=1 where Hello World runs - [GenerateFungeMethod("Programs/hello3d.b98")] - public static partial string HelloWorld3D(); - } -} +using Esolang.Funge; +using System.Text; + +// string return (file-based) +Console.WriteLine($"{nameof(FungeSample.HelloWorld)}: {FungeSample.HelloWorld()}"); + +// Task return (file-based) +Console.WriteLine($"{nameof(FungeSample.HelloWorldAsync)}: {await FungeSample.HelloWorldAsync()}"); + +// void with TextWriter output (file-based) +using var textWriter = new StringWriter(); +FungeSample.HelloWorldWriter(textWriter); +Console.WriteLine($"{nameof(FungeSample.HelloWorldWriter)}: {textWriter}"); + +// IEnumerable (file-based) +Console.WriteLine($"{nameof(FungeSample.HelloWorldBytes)}: {Encoding.UTF8.GetString(FungeSample.HelloWorldBytes().ToArray())}"); + +// IAsyncEnumerable (file-based) +Console.WriteLine($"{nameof(FungeSample.HelloWorldBytesAsync)}: {Encoding.UTF8.GetString(await ToByteArrayAsync(FungeSample.HelloWorldBytesAsync()))}"); + +// Inline source — same "Hello, World!" program embedded as a string literal +Console.WriteLine($"{nameof(FungeSample.HelloWorldInline)}: {FungeSample.HelloWorldInline()}"); + +// 3D (Trefunge) — layer Z=0 uses 'l' to jump into layer Z=1 where Hello World runs +Console.WriteLine($"{nameof(FungeSample.HelloWorld3D)}: {FungeSample.HelloWorld3D()}"); + +static async Task ToByteArrayAsync(IAsyncEnumerable source) +{ + var list = new List(); + await foreach (var b in source) + list.Add(b); + return [.. list]; +} + +namespace Esolang.Funge +{ + partial class FungeSample + { + [GenerateFungeMethod("Programs/hello.b98")] + public static partial string HelloWorld(); + + [GenerateFungeMethod("Programs/hello.b98")] + public static partial Task HelloWorldAsync(); + + [GenerateFungeMethod("Programs/hello.b98")] + public static partial void HelloWorldWriter(System.IO.TextWriter output); + + [GenerateFungeMethod("Programs/hello.b98")] + public static partial IEnumerable HelloWorldBytes(); + + [GenerateFungeMethod("Programs/hello.b98")] + public static partial IAsyncEnumerable HelloWorldBytesAsync(); + + // InlineSource: no .b98 file needed — Funge-98 code is embedded directly + [GenerateFungeMethod(InlineSource = "64+\"!dlroW ,olleH\">:#,_@")] + public static partial string HelloWorldInline(); + + // 3D (Trefunge): layer Z=0 executes 'l' (go low) to enter layer Z=1 where Hello World runs + [GenerateFungeMethod("Programs/hello3d.b98")] + public static partial string HelloWorld3D(); + } +} From db9f79b7c98df15cb911ce247835b99173681c30 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 10:04:14 +0900 Subject: [PATCH 12/17] Split generator runtime dispatch and add sync cancellation support --- CHANGELOG.md | 1 + Generator.Tests/FungeMethodGeneratorTests.cs | 140 ++++++++++++++++++ Generator/MethodGenerator.Runtime.cs | 144 ++++++++++++++++++- Generator/MethodGenerator.cs | 105 +++++++++----- 4 files changed, 352 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08251ac..54521e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Processor`: `y` now includes command-line arguments and environment variables, uses Funge-98 date/time encoding (base-256), reports least-stack-area bounds as relative extents, and aligns positive-`c` pick behavior with full-stack semantics. - `dotnet-funge` (`Esolang.Funge.Interpreter`): processor construction now passes command-line arguments and environment variables for `y` system-info reporting. - `Esolang.Funge.Generator`: generated runtime now supports concurrency (`t`), stack stack operations (`{` / `}` / `u`), `y` system-info, and applies storage offset semantics to `g` / `p` and file I/O least-point handling. +- `Esolang.Funge.Generator`: method return handling now dispatches via runtime facade APIs (`RunSync` / `RunTask*` / `RunValueTask*` / `RunEnumerable` / `RunAsyncEnumerable`), and synchronous signatures can cooperatively cancel infinite execution via `CancellationToken`. - Docs: updated package README compliance tables and added 3D notes/examples (including `\f` layer separator guidance). - Repository hygiene: ignore `*.csproj.lscache` (C# Dev Kit cache) to avoid generated noise in working trees. diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index 836ff21..d71ca3d 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -932,6 +932,146 @@ await Task.Factory.StartNew(() => }, TestCancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } + [TestMethod] + public void Generated_TaskReturn_UsesTaskRuntimeFacade() + { + var source = """ + using Esolang.Funge; + using System.Threading.Tasks; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("task-facade.b98")] + public static partial Task Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("task-facade.b98", "@")]); + AssertNoErrors(diag, comp); + + var generated = comp.SyntaxTrees + .Select(static t => t.ToString()) + .Single(static text => text.Contains("Generated from: task-facade.b98", StringComparison.Ordinal)); + Assert.Contains("FungeRuntime.RunTask(", generated); + } + + [TestMethod] + public void Generated_ValueTaskStringReturn_UsesValueTaskRuntimeFacade() + { + var source = """ + using Esolang.Funge; + using System.Threading.Tasks; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("valuetask-facade.b98")] + public static partial ValueTask Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("valuetask-facade.b98", "@")]); + AssertNoErrors(diag, comp); + + var generated = comp.SyntaxTrees + .Select(static t => t.ToString()) + .Single(static text => text.Contains("Generated from: valuetask-facade.b98", StringComparison.Ordinal)); + Assert.Contains("FungeRuntime.RunValueTaskString(", generated); + } + + [TestMethod] + public async Task Runtime_AsyncEnumerableByte_ReturnsOutputBytes() + { + const string program = "\"A\",@"; + + var source = """ + using Esolang.Funge; + using System.Collections.Generic; + using System.Threading; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("async-enumerable.b98")] + public static partial IAsyncEnumerable Run(CancellationToken cancellationToken = default); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("async-enumerable.b98", program)]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp, TestCancellationToken); + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + var stream = (IAsyncEnumerable?)m.Invoke(null, [TestCancellationToken]); + Assert.IsNotNull(stream); + + var bytes = new List(); + var enumerator = stream!.GetAsyncEnumerator(TestCancellationToken); + try + { + while (await enumerator.MoveNextAsync()) + bytes.Add(enumerator.Current); + } + finally + { + await enumerator.DisposeAsync(); + } + + CollectionAssert.AreEqual(new byte[] { (byte)'A' }, bytes); + } + + [TestMethod] + public void Generated_SyncWithCancellationToken_UsesRunSyncWithToken() + { + var source = """ + using Esolang.Funge; + using System.Threading; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("sync-token.b98")] + public static partial int Run(CancellationToken cancellationToken = default); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("sync-token.b98", "@")]); + AssertNoErrors(diag, comp); + + var generated = comp.SyntaxTrees + .Select(static t => t.ToString()) + .Single(static text => text.Contains("Generated from: sync-token.b98", StringComparison.Ordinal)); + Assert.Contains("FungeRuntime.RunSync(", generated); + Assert.Contains("cancellationToken", generated); + } + + [TestMethod] + public void Runtime_SyncCancellationToken_CancelsInfiniteLoop() + { + var source = """ + using Esolang.Funge; + using System.Threading; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("infinite-loop.b98")] + public static partial int Run(CancellationToken cancellationToken = default); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("infinite-loop.b98", ">")]); + AssertNoErrors(diag, comp); + + var asm = Emit(comp, TestCancellationToken); + var t = asm.GetType("TestProject.TestClass")!; + var m = t.GetMethod("Run")!; + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var ex = Assert.Throws(() => m.Invoke(null, [cts.Token])); + Assert.IsNotNull(ex?.InnerException); + Assert.IsInstanceOfType(ex!.InnerException, typeof(OperationCanceledException)); + } + [TestMethod] public void Diagnostic_SourceFileNotFound_FG0004() { diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index fbb8c3a..eede8ae 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -22,11 +22,150 @@ static string BuildRuntimeSource() => """ using System.Diagnostics; using System.IO; using System.Linq; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; namespace Esolang.Funge.__Generated { internal static class FungeRuntime { + internal static int RunSync( + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, + TextReader input, + TextWriter output, + bool hasInput, + bool hasOutput, + CancellationToken cancellationToken = default) + { + return Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken); + } + + internal static string RunString( + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, + TextReader input, + bool hasInput, + bool hasOutput, + CancellationToken cancellationToken = default) + { + using var output = new StringWriter(); + Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken); + return output.ToString(); + } + + internal static IEnumerable RunEnumerable( + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, + TextReader input, + bool hasInput, + bool hasOutput, + CancellationToken cancellationToken = default) + { + var bytes = System.Text.Encoding.UTF8.GetBytes( + RunString(cells, minX, minY, minZ, maxX, maxY, maxZ, input, hasInput, hasOutput, cancellationToken)); + foreach (var b in bytes) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return b; + } + } + + internal static async IAsyncEnumerable RunAsyncEnumerable( + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, + TextReader input, + bool hasInput, + bool hasOutput, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var bytes = System.Text.Encoding.UTF8.GetBytes( + RunString(cells, minX, minY, minZ, maxX, maxY, maxZ, input, hasInput, hasOutput, cancellationToken)); + + foreach (var b in bytes) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return b; + await Task.Yield(); + } + } + + internal static Task RunTask( + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, + TextReader input, + TextWriter output, + bool hasInput, + bool hasOutput, + CancellationToken cancellationToken = default) + { + return Task.Run(() => + { + Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken); + }, cancellationToken); + } + + internal static Task RunTaskInt( + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, + TextReader input, + bool hasInput, + bool hasOutput, + CancellationToken cancellationToken = default) + { + return Task.Run(() => + Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, TextWriter.Null, hasInput, hasOutput, cancellationToken), + cancellationToken); + } + + internal static Task RunTaskString( + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, + TextReader input, + bool hasInput, + bool hasOutput, + CancellationToken cancellationToken = default) + { + return Task.Run(() => + RunString(cells, minX, minY, minZ, maxX, maxY, maxZ, input, hasInput, hasOutput, cancellationToken), + cancellationToken); + } + + internal static ValueTask RunValueTask( + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, + TextReader input, + TextWriter output, + bool hasInput, + bool hasOutput, + CancellationToken cancellationToken = default) + { + return new ValueTask(RunTask(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken)); + } + + internal static ValueTask RunValueTaskInt( + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, + TextReader input, + bool hasInput, + bool hasOutput, + CancellationToken cancellationToken = default) + { + return new ValueTask(RunTaskInt(cells, minX, minY, minZ, maxX, maxY, maxZ, input, hasInput, hasOutput, cancellationToken)); + } + + internal static ValueTask RunValueTaskString( + Dictionary<(int, int, int), int> cells, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ, + TextReader input, + bool hasInput, + bool hasOutput, + CancellationToken cancellationToken = default) + { + return new ValueTask(RunTaskString(cells, minX, minY, minZ, maxX, maxY, maxZ, input, hasInput, hasOutput, cancellationToken)); + } + private sealed class RuntimeStackStack { private readonly LinkedList> _stacks = new LinkedList>(); @@ -105,7 +244,8 @@ internal static int Run( TextReader input, TextWriter output, bool hasInput, - bool hasOutput) + bool hasOutput, + CancellationToken cancellationToken = default) { var rng = new Random(); var commandLineArguments = Environment.GetCommandLineArgs(); @@ -928,9 +1068,11 @@ void ExecuteInstruction(RuntimeIp ip, LinkedListNode ipNode, ref bool while (ips.Count > 0 && !quit) { + cancellationToken.ThrowIfCancellationRequested(); var node = ips.First; while (node != null && !quit) { + cancellationToken.ThrowIfCancellationRequested(); var nextNode = node.Next; var ip = node.Value; diff --git a/Generator/MethodGenerator.cs b/Generator/MethodGenerator.cs index 74613d5..7ad4f86 100644 --- a/Generator/MethodGenerator.cs +++ b/Generator/MethodGenerator.cs @@ -493,79 +493,110 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin $"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; EmitSpaceData(sb, space); switch (binding.ReturnKind) { - case ReturnKind.Int: - case ReturnKind.String: - case ReturnKind.TaskInt: - case ReturnKind.TaskString: - case ReturnKind.ValueTaskInt: - case ReturnKind.ValueTaskString: - case ReturnKind.EnumerableByte: - case ReturnKind.AsyncEnumerableByte: - sb.AppendLine(" var __fungeOutput = new global::System.IO.StringWriter();"); - if (binding.ReturnKind is ReturnKind.Int or ReturnKind.TaskInt or ReturnKind.ValueTaskInt) - { - sb.AppendLine(" var __fungeExitCode = global::Esolang.Funge.__Generated.FungeRuntime.Run("); - sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, __fungeOutput, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")});"); - } - else - { - EmitRuntimeRunCall(sb, inputExpr, "__fungeOutput", binding.HasExplicitInput, binding.HasExplicitOutput); - } - break; case ReturnKind.Void: - case ReturnKind.Task: - case ReturnKind.ValueTask: { if (binding.OutputKind == OutputKind.PipeWriter) { sb.AppendLine($" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true);"); - EmitRuntimeRunCall(sb, inputExpr, "__fungeOutput", binding.HasExplicitInput, binding.HasExplicitOutput); + sb.AppendLine(" global::Esolang.Funge.__Generated.FungeRuntime.RunSync("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, __fungeOutput, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); } else { var outExpr = binding.OutputKind == OutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; - EmitRuntimeRunCall(sb, inputExpr, outExpr, binding.HasExplicitInput, binding.HasExplicitOutput); + sb.AppendLine(" global::Esolang.Funge.__Generated.FungeRuntime.RunSync("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {outExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); } break; } - } - switch (binding.ReturnKind) - { case ReturnKind.Int: - sb.AppendLine(" return __fungeExitCode;"); + sb.AppendLine(" return global::Esolang.Funge.__Generated.FungeRuntime.RunSync("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, global::System.IO.TextWriter.Null, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); break; + case ReturnKind.String: - sb.AppendLine(" return __fungeOutput.ToString();"); + sb.AppendLine(" return global::Esolang.Funge.__Generated.FungeRuntime.RunString("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); break; + case ReturnKind.Task: - sb.AppendLine(" return global::System.Threading.Tasks.Task.CompletedTask;"); - break; + { + if (binding.OutputKind == OutputKind.PipeWriter) + { + sb.AppendLine($" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true);"); + sb.AppendLine(" return global::Esolang.Funge.__Generated.FungeRuntime.RunTask("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, __fungeOutput, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); + } + else + { + var outExpr = binding.OutputKind == OutputKind.TextWriter + ? binding.OutputExpression + : "global::System.IO.TextWriter.Null"; + sb.AppendLine(" return global::Esolang.Funge.__Generated.FungeRuntime.RunTask("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {outExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); + } + break; + } + case ReturnKind.TaskInt: - sb.AppendLine(" return global::System.Threading.Tasks.Task.FromResult(__fungeExitCode);"); + sb.AppendLine(" return global::Esolang.Funge.__Generated.FungeRuntime.RunTaskInt("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); break; + case ReturnKind.TaskString: - sb.AppendLine(" return global::System.Threading.Tasks.Task.FromResult(__fungeOutput.ToString());"); + sb.AppendLine(" return global::Esolang.Funge.__Generated.FungeRuntime.RunTaskString("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); break; + case ReturnKind.ValueTask: - sb.AppendLine(" return default(global::System.Threading.Tasks.ValueTask);"); - break; + { + if (binding.OutputKind == OutputKind.PipeWriter) + { + sb.AppendLine($" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true);"); + sb.AppendLine(" return global::Esolang.Funge.__Generated.FungeRuntime.RunValueTask("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, __fungeOutput, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); + } + else + { + var outExpr = binding.OutputKind == OutputKind.TextWriter + ? binding.OutputExpression + : "global::System.IO.TextWriter.Null"; + sb.AppendLine(" return global::Esolang.Funge.__Generated.FungeRuntime.RunValueTask("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {outExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); + } + break; + } + case ReturnKind.ValueTaskInt: - sb.AppendLine(" return new global::System.Threading.Tasks.ValueTask(__fungeExitCode);"); + sb.AppendLine(" return global::Esolang.Funge.__Generated.FungeRuntime.RunValueTaskInt("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); break; + case ReturnKind.ValueTaskString: - sb.AppendLine(" return new global::System.Threading.Tasks.ValueTask(__fungeOutput.ToString());"); + sb.AppendLine(" return global::Esolang.Funge.__Generated.FungeRuntime.RunValueTaskString("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr});"); break; + case ReturnKind.EnumerableByte: + sb.AppendLine(" foreach (var __b in global::Esolang.Funge.__Generated.FungeRuntime.RunEnumerable("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}))"); + sb.AppendLine(" yield return __b;"); + break; + case ReturnKind.AsyncEnumerableByte: - sb.AppendLine(" foreach (var __b in global::System.Text.Encoding.UTF8.GetBytes(__fungeOutput.ToString()))"); + sb.AppendLine(" await foreach (var __b in global::Esolang.Funge.__Generated.FungeRuntime.RunAsyncEnumerable("); + sb.AppendLine($" __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}))"); sb.AppendLine(" yield return __b;"); break; } From 9a5d21b02433af93a69db28c47274eb9b996a0e4 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 10:20:30 +0900 Subject: [PATCH 13/17] Emit minimal runtime facades and clean raw-literal layout --- CHANGELOG.md | 1 + Generator.Tests/FungeMethodGeneratorTests.cs | 53 ++++++ Generator/MethodGenerator.Runtime.cs | 170 +++++++++++++++---- Generator/MethodGenerator.cs | 19 ++- 4 files changed, 213 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54521e5..441c005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ The format is based on Keep a Changelog. - `dotnet-funge` (`Esolang.Funge.Interpreter`): processor construction now passes command-line arguments and environment variables for `y` system-info reporting. - `Esolang.Funge.Generator`: generated runtime now supports concurrency (`t`), stack stack operations (`{` / `}` / `u`), `y` system-info, and applies storage offset semantics to `g` / `p` and file I/O least-point handling. - `Esolang.Funge.Generator`: method return handling now dispatches via runtime facade APIs (`RunSync` / `RunTask*` / `RunValueTask*` / `RunEnumerable` / `RunAsyncEnumerable`), and synchronous signatures can cooperatively cancel infinite execution via `CancellationToken`. +- `Esolang.Funge.Generator`: runtime source emission now includes only the facade methods required by generated signatures (Piet-style minimal runtime emission), and facade source blocks are generated with raw string literals for cleaner output layout. - Docs: updated package README compliance tables and added 3D notes/examples (including `\f` layer separator guidance). - Repository hygiene: ignore `*.csproj.lscache` (C# Dev Kit cache) to avoid generated noise in working trees. diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index d71ca3d..397c158 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -1043,6 +1043,59 @@ partial class TestClass Assert.Contains("cancellationToken", generated); } + [TestMethod] + public void Generated_Runtime_EmitsOnlyRequiredFacades_ForIntReturn() + { + var source = """ + using Esolang.Funge; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("minimal-int.b98")] + public static partial int Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("minimal-int.b98", "@")]); + AssertNoErrors(diag, comp); + + var runtime = comp.SyntaxTrees + .Select(static t => t.ToString()) + .Single(static text => text.Contains("internal static class FungeRuntime", StringComparison.Ordinal)); + + Assert.Contains("internal static int RunSync(", runtime); + Assert.IsFalse(runtime.Contains("internal static Task RunTask(", StringComparison.Ordinal)); + Assert.IsFalse(runtime.Contains("internal static ValueTask RunValueTaskString(", StringComparison.Ordinal)); + Assert.IsFalse(runtime.Contains("internal static async IAsyncEnumerable RunAsyncEnumerable(", StringComparison.Ordinal)); + } + + [TestMethod] + public void Generated_Runtime_EmitsOnlyRequiredFacades_ForValueTaskString() + { + var source = """ + using Esolang.Funge; + using System.Threading.Tasks; + namespace TestProject; + partial class TestClass + { + [GenerateFungeMethod("minimal-vts.b98")] + public static partial ValueTask Run(); + } + """; + RunGenerators(source, out var comp, out var diag, + additionalFiles: [("minimal-vts.b98", "@")]); + AssertNoErrors(diag, comp); + + var runtime = comp.SyntaxTrees + .Select(static t => t.ToString()) + .Single(static text => text.Contains("internal static class FungeRuntime", StringComparison.Ordinal)); + + Assert.Contains("internal static ValueTask RunValueTaskString(", runtime); + Assert.IsFalse(runtime.Contains("internal static int RunSync(", StringComparison.Ordinal)); + Assert.IsFalse(runtime.Contains("internal static Task RunTaskInt(", StringComparison.Ordinal)); + Assert.IsFalse(runtime.Contains("internal static IEnumerable RunEnumerable(", StringComparison.Ordinal)); + } + [TestMethod] public void Runtime_SyncCancellationToken_CancelsInfiniteLoop() { diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index eede8ae..2266114 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -6,30 +6,36 @@ partial class MethodGenerator { const string FungeRuntimeFileName = "FungeRuntime.g.cs"; - static void EmitRuntimeIfNeeded(Microsoft.CodeAnalysis.SourceProductionContext ctx, bool needed) + [System.Flags] + enum RuntimeFacadeFeatures { - if (!needed) return; - ctx.AddSource(FungeRuntimeFileName, BuildRuntimeSource()); + None = 0, + RunSync = 1 << 0, + RunString = 1 << 1, + RunEnumerable = 1 << 2, + RunAsyncEnumerable = 1 << 3, + RunTask = 1 << 4, + RunTaskInt = 1 << 5, + RunTaskString = 1 << 6, + RunValueTask = 1 << 7, + RunValueTaskInt = 1 << 8, + RunValueTaskString = 1 << 9, } - static string BuildRuntimeSource() => """ - // - #nullable enable - #pragma warning disable CS1591 - using System; - using System.Collections; - using System.Collections.Generic; - using System.Diagnostics; - using System.IO; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Threading; - using System.Threading.Tasks; + static void EmitRuntimeIfNeeded(Microsoft.CodeAnalysis.SourceProductionContext ctx, RuntimeFacadeFeatures features) + { + if (features == RuntimeFacadeFeatures.None) return; + ctx.AddSource(FungeRuntimeFileName, BuildRuntimeSource(features)); + } - namespace Esolang.Funge.__Generated + static string BuildRuntimeFacadeMethods(RuntimeFacadeFeatures features) + { + var sb = new StringBuilder(); + + if ((features & RuntimeFacadeFeatures.RunSync) != 0) { - internal static class FungeRuntime - { + sb.Append(""" + internal static int RunSync( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -42,6 +48,13 @@ internal static int RunSync( return Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken); } + """); + } + + if ((features & RuntimeFacadeFeatures.RunString) != 0) + { + sb.Append(""" + internal static string RunString( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -55,6 +68,13 @@ internal static string RunString( return output.ToString(); } + """); + } + + if ((features & RuntimeFacadeFeatures.RunEnumerable) != 0) + { + sb.Append(""" + internal static IEnumerable RunEnumerable( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -63,8 +83,9 @@ internal static IEnumerable RunEnumerable( bool hasOutput, CancellationToken cancellationToken = default) { - var bytes = System.Text.Encoding.UTF8.GetBytes( - RunString(cells, minX, minY, minZ, maxX, maxY, maxZ, input, hasInput, hasOutput, cancellationToken)); + using var output = new StringWriter(); + Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken); + var bytes = System.Text.Encoding.UTF8.GetBytes(output.ToString()); foreach (var b in bytes) { cancellationToken.ThrowIfCancellationRequested(); @@ -72,6 +93,13 @@ internal static IEnumerable RunEnumerable( } } + """); + } + + if ((features & RuntimeFacadeFeatures.RunAsyncEnumerable) != 0) + { + sb.Append(""" + internal static async IAsyncEnumerable RunAsyncEnumerable( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -80,8 +108,9 @@ internal static async IAsyncEnumerable RunAsyncEnumerable( bool hasOutput, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var bytes = System.Text.Encoding.UTF8.GetBytes( - RunString(cells, minX, minY, minZ, maxX, maxY, maxZ, input, hasInput, hasOutput, cancellationToken)); + using var output = new StringWriter(); + Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken); + var bytes = System.Text.Encoding.UTF8.GetBytes(output.ToString()); foreach (var b in bytes) { @@ -91,6 +120,13 @@ internal static async IAsyncEnumerable RunAsyncEnumerable( } } + """); + } + + if ((features & RuntimeFacadeFeatures.RunTask) != 0) + { + sb.Append(""" + internal static Task RunTask( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -106,6 +142,13 @@ internal static Task RunTask( }, cancellationToken); } + """); + } + + if ((features & RuntimeFacadeFeatures.RunTaskInt) != 0) + { + sb.Append(""" + internal static Task RunTaskInt( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -119,6 +162,13 @@ internal static Task RunTaskInt( cancellationToken); } + """); + } + + if ((features & RuntimeFacadeFeatures.RunTaskString) != 0) + { + sb.Append(""" + internal static Task RunTaskString( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -128,10 +178,20 @@ internal static Task RunTaskString( CancellationToken cancellationToken = default) { return Task.Run(() => - RunString(cells, minX, minY, minZ, maxX, maxY, maxZ, input, hasInput, hasOutput, cancellationToken), - cancellationToken); + { + using var output = new StringWriter(); + Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken); + return output.ToString(); + }, cancellationToken); } + """); + } + + if ((features & RuntimeFacadeFeatures.RunValueTask) != 0) + { + sb.Append(""" + internal static ValueTask RunValueTask( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -141,9 +201,19 @@ internal static ValueTask RunValueTask( bool hasOutput, CancellationToken cancellationToken = default) { - return new ValueTask(RunTask(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken)); + return new ValueTask(Task.Run(() => + { + Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken); + }, cancellationToken)); } + """); + } + + if ((features & RuntimeFacadeFeatures.RunValueTaskInt) != 0) + { + sb.Append(""" + internal static ValueTask RunValueTaskInt( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -152,9 +222,18 @@ internal static ValueTask RunValueTaskInt( bool hasOutput, CancellationToken cancellationToken = default) { - return new ValueTask(RunTaskInt(cells, minX, minY, minZ, maxX, maxY, maxZ, input, hasInput, hasOutput, cancellationToken)); + return new ValueTask(Task.Run(() => + Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, TextWriter.Null, hasInput, hasOutput, cancellationToken), + cancellationToken)); } + """); + } + + if ((features & RuntimeFacadeFeatures.RunValueTaskString) != 0) + { + sb.Append(""" + internal static ValueTask RunValueTaskString( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -163,9 +242,42 @@ internal static ValueTask RunValueTaskString( bool hasOutput, CancellationToken cancellationToken = default) { - return new ValueTask(RunTaskString(cells, minX, minY, minZ, maxX, maxY, maxZ, input, hasInput, hasOutput, cancellationToken)); + return new ValueTask(Task.Run(() => + { + using var output = new StringWriter(); + Run(cells, minX, minY, minZ, maxX, maxY, maxZ, input, output, hasInput, hasOutput, cancellationToken); + return output.ToString(); + }, cancellationToken)); } + """); + } + + return sb.ToString(); + } + + static string BuildRuntimeSource(RuntimeFacadeFeatures features) => string.Concat(""" + // + #nullable enable + #pragma warning disable CS1591 + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + + namespace Esolang.Funge.__Generated + { + internal static class FungeRuntime + { + + """, + BuildRuntimeFacadeMethods(features),""" + private sealed class RuntimeStackStack { private readonly LinkedList> _stacks = new LinkedList>(); @@ -1096,5 +1208,5 @@ void ExecuteInstruction(RuntimeIp ip, LinkedListNode ipNode, ref bool } } } - """; + """); } diff --git a/Generator/MethodGenerator.cs b/Generator/MethodGenerator.cs index 7ad4f86..3d6e49a 100644 --- a/Generator/MethodGenerator.cs +++ b/Generator/MethodGenerator.cs @@ -147,6 +147,7 @@ internal sealed class {{AttributeName}} : Attribute var methodSb = new StringBuilder(GeneratedMethodsFileHeader); var emittedCount = 0; + var runtimeFeatures = RuntimeFacadeFeatures.None; foreach (var syntaxCtx in sources) { @@ -270,15 +271,31 @@ internal sealed class {{AttributeName}} : Attribute var emitted = EmitMethod(symbol, method, space, binding, projDir, displayPath); methodSb.AppendLine(emitted); emittedCount++; + runtimeFeatures |= GetRuntimeFacadeFeatures(binding.ReturnKind); } if (emittedCount > 0) ctx.AddSource(GeneratedMethodsFileName, methodSb.ToString()); - EmitRuntimeIfNeeded(ctx, emittedCount > 0); + EmitRuntimeIfNeeded(ctx, runtimeFeatures); }); } + static RuntimeFacadeFeatures GetRuntimeFacadeFeatures(ReturnKind 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, + _ => RuntimeFacadeFeatures.None, + }; + // ----------------------------------------------------------------------- // Signature binding // ----------------------------------------------------------------------- From 632ef1b27863cba4255208ec814cad1ea98a14e2 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 10:24:22 +0900 Subject: [PATCH 14/17] Hide generated runtime internals from IntelliSense --- CHANGELOG.md | 1 + Generator/MethodGenerator.Runtime.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 441c005..fdd1b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Generator`: generated runtime now supports concurrency (`t`), stack stack operations (`{` / `}` / `u`), `y` system-info, and applies storage offset semantics to `g` / `p` and file I/O least-point handling. - `Esolang.Funge.Generator`: method return handling now dispatches via runtime facade APIs (`RunSync` / `RunTask*` / `RunValueTask*` / `RunEnumerable` / `RunAsyncEnumerable`), and synchronous signatures can cooperatively cancel infinite execution via `CancellationToken`. - `Esolang.Funge.Generator`: runtime source emission now includes only the facade methods required by generated signatures (Piet-style minimal runtime emission), and facade source blocks are generated with raw string literals for cleaner output layout. +- `Esolang.Funge.Generator`: generated runtime class and internal runtime entry methods are now annotated with `EditorBrowsable(EditorBrowsableState.Never)` to reduce IntelliSense surface for internal-use APIs. - Docs: updated package README compliance tables and added 3D notes/examples (including `\f` layer separator guidance). - Repository hygiene: ignore `*.csproj.lscache` (C# Dev Kit cache) to avoid generated noise in working trees. diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index 2266114..7fd470a 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -36,6 +36,7 @@ static string BuildRuntimeFacadeMethods(RuntimeFacadeFeatures features) { sb.Append(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static int RunSync( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -55,6 +56,7 @@ internal static int RunSync( { sb.Append(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static string RunString( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -75,6 +77,7 @@ internal static string RunString( { sb.Append(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static IEnumerable RunEnumerable( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -100,6 +103,7 @@ internal static IEnumerable RunEnumerable( { sb.Append(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static async IAsyncEnumerable RunAsyncEnumerable( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -127,6 +131,7 @@ internal static async IAsyncEnumerable RunAsyncEnumerable( { sb.Append(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static Task RunTask( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -149,6 +154,7 @@ internal static Task RunTask( { sb.Append(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static Task RunTaskInt( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -169,6 +175,7 @@ internal static Task RunTaskInt( { sb.Append(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static Task RunTaskString( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -192,6 +199,7 @@ internal static Task RunTaskString( { sb.Append(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static ValueTask RunValueTask( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -214,6 +222,7 @@ internal static ValueTask RunValueTask( { sb.Append(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static ValueTask RunValueTaskInt( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -234,6 +243,7 @@ internal static ValueTask RunValueTaskInt( { sb.Append(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static ValueTask RunValueTaskString( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, @@ -272,6 +282,7 @@ static string BuildRuntimeSource(RuntimeFacadeFeatures features) => string.Conca namespace Esolang.Funge.__Generated { + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static class FungeRuntime { @@ -350,6 +361,7 @@ internal RuntimeIp CreateChild(int newId) } } + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static int Run( Dictionary<(int, int, int), int> cells, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, From 7f89e90d2f83c75528b9c3245a88c89c0bf599ad Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 10:25:44 +0900 Subject: [PATCH 15/17] =?UTF-8?q?=E5=90=8D=E5=89=8D=E7=A9=BA=E9=96=93?= =?UTF-8?q?=E3=81=AE=E7=9C=81=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs index 6da1c86..b6abc91 100644 --- a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs +++ b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs @@ -43,7 +43,7 @@ partial class FungeSample public static partial Task HelloWorldAsync(); [GenerateFungeMethod("Programs/hello.b98")] - public static partial void HelloWorldWriter(System.IO.TextWriter output); + public static partial void HelloWorldWriter(TextWriter output); [GenerateFungeMethod("Programs/hello.b98")] public static partial IEnumerable HelloWorldBytes(); From 6a9bae767e475d2580670ee6d06b6a8d5df1677d Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 11:04:46 +0900 Subject: [PATCH 16/17] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=A4=E3=83=9E?= =?UTF-8?q?=E3=83=AA=E3=82=B3=E3=83=B3=E3=82=B9=E3=83=88=E3=83=A9=E3=82=AF?= =?UTF-8?q?=E3=82=BF=E3=81=A8=20dotnet=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Generator/MethodGenerator.Runtime.cs | 2 +- Parser/FungeVector.cs | 20 ++++------ Processor/FungeProcessor.cs | 56 +++++++++++++--------------- 3 files changed, 34 insertions(+), 44 deletions(-) diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index 7fd470a..bcd25da 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -287,7 +287,7 @@ internal static class FungeRuntime { """, - BuildRuntimeFacadeMethods(features),""" + BuildRuntimeFacadeMethods(features), """ private sealed class RuntimeStackStack { diff --git a/Parser/FungeVector.cs b/Parser/FungeVector.cs index a80fc88..1b559c3 100644 --- a/Parser/FungeVector.cs +++ b/Parser/FungeVector.cs @@ -1,26 +1,22 @@ +using System.Diagnostics; + namespace Esolang.Funge.Parser; /// /// Represents a 3D integer vector used for positions and deltas in Funge-98. /// -public readonly struct FungeVector : IEquatable +/// Initializes a new with the given components. +[DebuggerDisplay($"{{{nameof(ToString)}}}")] +public readonly struct FungeVector(int x, int y, int z) : IEquatable { /// The X component. - public int X { get; } + public int X { get; } = x; /// The Y component. - public int Y { get; } + public int Y { get; } = y; /// The Z component. - public int Z { get; } - - /// Initializes a new with the given components. - public FungeVector(int x, int y, int z) - { - X = x; - Y = y; - Z = z; - } + public int Z { get; } = z; /// Initializes a new in 2D (Z=0). public FungeVector(int x, int y) : this(x, y, 0) { } diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index 74dfd2f..b031c60 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -12,44 +12,38 @@ namespace Esolang.Funge.Processor; /// Fingerprints ((/)) reflect (not implemented). /// Includes Trefunge 3-D direction instructions (h/l/m). /// -public sealed partial class FungeProcessor : ITextProcessor +/// +/// 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) : ITextProcessor { - private readonly FungeSpace _space; - private readonly TextWriter _output; - private readonly TextReader _input; - private readonly string[] _commandLineArguments; - private readonly string[] _environmentVariables; + 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()) +#pragma warning disable IDE0305 // コレクションの初期化を簡略化します + .ToArray(); +#pragma warning restore IDE0305 // コレクションの初期化を簡略化します + private readonly string[] _environmentVariables = [.. environmentVariables + ?? Environment.GetEnvironmentVariables() + .Cast() + .Select(static entry => $"{entry.Key}={entry.Value}")]; private readonly Random _random = new(); private int _nextIpId; /// public FungeSpace Program => _space; - /// - /// 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 FungeProcessor( - FungeSpace space, - TextWriter? output = null, - TextReader? input = null, - IEnumerable? commandLineArguments = null, - IEnumerable? environmentVariables = null) - { - _space = space; - _output = output ?? Console.Out; - _input = input ?? Console.In; - _commandLineArguments = (commandLineArguments ?? Environment.GetCommandLineArgs()).ToArray(); - _environmentVariables = (environmentVariables ?? Environment.GetEnvironmentVariables() - .Cast() - .Select(static entry => $"{entry.Key}={entry.Value}")) - .ToArray(); - } - /// /// Runs the Funge-98 program and returns the process exit code. /// The program starts with a single IP at (0,0) moving East. From 8ff91d4dd0d935185f0d9f55c6d3248abd5c1bd7 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 8 May 2026 11:21:18 +0900 Subject: [PATCH 17/17] Prepare 1.1.0 release --- CHANGELOG.md | 3 +++ Directory.Build.props | 6 +++--- README.md | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd1b7e..311c099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on Keep a Changelog. ## [Unreleased] +## [1.1.0] - 2026-05-08 + ### Added - `Esolang.Processor.Abstractions` (`Esolang.Processor` namespace): shared execution abstractions package (`IProcessor`, `ITextProcessor`, `IPipeProcessor`). @@ -35,6 +37,7 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Generator`: generated runtime class and internal runtime entry methods are now annotated with `EditorBrowsable(EditorBrowsableState.Never)` to reduce IntelliSense surface for internal-use APIs. - Docs: updated package README compliance tables and added 3D notes/examples (including `\f` layer separator guidance). - Repository hygiene: ignore `*.csproj.lscache` (C# Dev Kit cache) to avoid generated noise in working trees. +- Build/package baseline: incremented `AssemblyVersion` / `FileVersion` to `1.1.0.3` and `Version` to `1.1.0`. ## [1.0.1] - 2026-05-07 diff --git a/Directory.Build.props b/Directory.Build.props index 8743322..ebb6abd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,9 +3,9 @@ enable enable 14 - 1.0.1.2 - 1.0.1.2 - 1.0.1 + 1.1.0.3 + 1.1.0.3 + 1.1.0 https://github.com/Esolang-NET/Funge/ https://github.com/Esolang-NET/Funge.git true diff --git a/README.md b/README.md index 923dffa..ed30492 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Details: dotnet add package Esolang.Funge.Generator dotnet add package Esolang.Funge.Parser dotnet add package Esolang.Funge.Processor -dotnet tool install -g dotnet-funge --prerelease +dotnet tool install -g dotnet-funge ``` ## Choose Package