diff --git a/.editorconfig b/.editorconfig index 60ca27f..d1cc9ea 100644 --- a/.editorconfig +++ b/.editorconfig @@ -39,7 +39,6 @@ dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning # Accessibility modifier settings -dotnet_style_require_accessibility_modifiers = for_non_interface_members # Expression-level preferences dotnet_style_coalesce_expression = true:warning @@ -236,7 +235,81 @@ dotnet_style_qualification_for_field = false:silent dotnet_style_qualification_for_property = false:warning dotnet_style_qualification_for_method = false:warning dotnet_style_qualification_for_event = false:warning -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning +# 関係演算子の優先順位が明確な場合は、かっこを使用しない +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:warning +# 関係演算子の優先順位が明確な場合は、かっこを使用しない +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:warning +# 関係演算子の優先順位が明確な場合は、かっこを使用しない +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:warning +# 算術演算子の優先順位が明確な場合はかっこを使用しない dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning +# IEnumerable list = new List() { 1, 2 };など、型が緩やかに一致する場合でもコレクション式を使用することを好みます。 対象となる型は、右側の型と一致するか、IEnumerable、ICollection、IList、IReadOnlyCollection、IReadOnlyListのいずれかの型である必要があります。 +dotnet_style_prefer_collection_expression = when_types_loosely_match:warning +# 不要な using ディレクティブを削除する (IDE0005) +dotnet_diagnostic.IDE0005.severity = warning +# is 式と型キャストの代わりにパターン マッチングを使用します。 +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +# 既定の修飾子である場合を除き、アクセシビリティ修飾子を優先します。 +dotnet_style_require_accessibility_modifiers = omit_if_default:warning +# 未使用のプライベート メンバーを削除する (IDE0051) +dotnet_diagnostic.IDE0051.severity = warning + +#region +# CA2012: ValueTask を正しく使用する必要があります +dotnet_diagnostic.CA2012.severity = warning +# CA2016: CancellationToken パラメーターを受け取るメソッドに渡す +dotnet_diagnostic.CA2016.severity = warning +# CA2200: スタックの詳細を保持するために再スローします。 +dotnet_diagnostic.CA2200.severity = warning +# CA2217: 列挙型に FlagsAttributeを付与しないでください。 +dotnet_diagnostic.CA2217.severity = warning +#endregion + +#region MSTest Analyzers + +# MSTEST0017: アサーション引数は正しい順序で渡す必要があります +dotnet_diagnostic.MSTEST0017.severity = warning +# MSTEST0020: TestInitialize メソッドよりもコンストラクターを優先する +dotnet_diagnostic.MSTEST0020.severity = warning +# MSTEST0021: TestCleanup メソッドよりも Dispose を優先する +dotnet_diagnostic.MSTEST0021.severity = warning +# MSTEST0022: Dispose メソッドよりも TestCleanup を優先する +dotnet_diagnostic.MSTEST0022.severity = warning +# MSTEST0023: ブール アサーションを否定しないこと +dotnet_diagnostic.MSTEST0023.severity = warning +# MSTEST0024: TestContext を静的メンバーに格納しない +dotnet_diagnostic.MSTEST0024.severity = warning +# MSTEST0025: 常に失敗するアサートではなく 'Assert.Fail' を使う +dotnet_diagnostic.MSTEST0025.severity = warning +# MSTEST0026: アサーションでの条件付きアクセスを回避する +dotnet_diagnostic.MSTEST0026.severity = warning +# MSTEST0029: パブリック メソッドをテスト メソッドにする必要がある +dotnet_diagnostic.MSTEST0029.severity = warning +# MSTEST0032: 条件が常に真であることがわかっているため、アサーションを確認するか削除してください +dotnet_diagnostic.MSTEST0032.severity = warning +# MSTEST0036: テスト クラス内でシャドウイングを使用しない +dotnet_diagnostic.MSTEST0036.severity = warning +# MSTEST0037: 適切な 'Assert' メソッドを使用する +dotnet_diagnostic.MSTEST0037.severity = warning +# MSTEST0038: 値型では 'Assert.AreSame' または 'Assert.AreNotSame' を使用しない +dotnet_diagnostic.MSTEST0038.severity = warning +# MSTEST0040: 'async void' コンテキスト内でアサートしない +dotnet_diagnostic.MSTEST0040.severity = warning +# MSTEST0044: DataTestMethod よりも TestMethod を優先する +dotnet_diagnostic.MSTEST0044.severity = warning +# MSTEST0045: タイムアウトに協調キャンセルを使用する +dotnet_diagnostic.MSTEST0045.severity = warning +# MSTEST0046: StringAssertではなくAssertを使います +dotnet_diagnostic.MSTEST0046.severity = warning +# MSTEST0049: Flow TestContext キャンセル トークンを使用する +dotnet_diagnostic.MSTEST0049.severity = warning +# MSTEST0051: Assert.Throws には 1 つのステートメントのみを含める必要があります +dotnet_diagnostic.MSTEST0051.severity = warning +# MSTEST0054: TestContext.CancellationToken からのキャンセル トークンを使用する +dotnet_diagnostic.MSTEST0054.severity = warning +# MSTEST0058: catch ブロック内の assert を避ける +dotnet_diagnostic.MSTEST0058.severity = warning +# MSTEST0061: ランタイム チェックの代わりに OSCondition 属性を使用する +dotnet_diagnostic.MSTEST0061.severity = warning + +#endregion \ No newline at end of file diff --git a/.github/workflows/aot.yml b/.github/workflows/aot.yml new file mode 100644 index 0000000..1caeb0f --- /dev/null +++ b/.github/workflows/aot.yml @@ -0,0 +1,61 @@ +name: native-aot-ci + +on: + push: + branches: [ main ] + paths: + - 'Interpreter/**' + - 'Processor/**' + - '.github/workflows/aot.yml' + pull_request: + branches: [ main ] + paths: + - 'Interpreter/**' + - 'Processor/**' + +env: + DOTNET_VERSION: '10.0.x' + PROJECT_PATH: 'Interpreter/Esolang.Funge.Interpreter.csproj' + +jobs: + build: + name: build-aot-${{ matrix.rid }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: windows-latest + rid: win-x64 + - os: ubuntu-latest + rid: linux-x64 + - os: macos-latest + rid: osx-x64 + - os: macos-latest + rid: osx-arm64 + - os: ubuntu-latest + rid: any + + steps: + - uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' && matrix.rid != 'any' + run: | + sudo apt-get update + sudo apt-get install -y clang zlib1g-dev + + - name: Pack + run: | + dotnet pack ${{ env.PROJECT_PATH }} -r ${{ matrix.rid }} -c Release -o artifacts/ + + - name: Upload Artifact (CI) + uses: actions/upload-artifact@v7 + with: + name: nupkg-ci-${{ matrix.rid }} + path: artifacts/* + retention-days: 1 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5796056..730a421 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -3,82 +3,115 @@ name: build and test on: push: pull_request: - branches: [ master ] + branches: [ main ] paths: - - '**.cs' - - '**.csproj' - - '**.slnx' - - '.github/workflows/**' + - '**.cs' + - '**.csproj' + - '**.slnx' + - '**.props' + - '**.targets' + - 'global.json' + - '.github/workflows/**' env: DOTNET_VERSION: '10.0.x' - NUGET_SOURCE: 'https://api.nuget.org/v3/index.json' jobs: build-and-test: - - name: build-and-test-${{matrix.os}} + name: build-and-test-${{ matrix.rid }} runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + include: + - os: ubuntu-latest + rid: linux-x64 + - os: windows-latest + rid: win-x64 + - os: macOS-latest + rid: osx-arm64 steps: - - uses: actions/checkout@v4 - - name: Setup .NET Core - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Install dependencies - run: dotnet restore --source "${{ env.NUGET_SOURCE }}" - - - name: Build - run: dotnet build --no-restore - - - name: Test - run: dotnet test --no-restore --verbosity normal - - - name: Tool E2E (pack/install/run) - if: ${{ matrix.os == 'ubuntu-latest' }} - shell: bash - run: | - set -euo pipefail - rm -rf .artifacts/tool-e2e - mkdir -p .artifacts/tool-e2e - - dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -o .artifacts/tool-e2e - - nupkg_path=$(find .artifacts/tool-e2e -maxdepth 1 -type f -name 'dotnet-funge.*.nupkg' | head -n 1) - if [ -z "$nupkg_path" ]; then - echo "Failed to find dotnet-funge nupkg in .artifacts/tool-e2e" - exit 1 - fi - nupkg_name=$(basename "$nupkg_path") - tool_version=${nupkg_name#dotnet-funge.} - tool_version=${tool_version%.nupkg} - echo "Detected tool version: $tool_version" - - dotnet tool install dotnet-funge \ - --tool-path .artifacts/tool-e2e/path \ - --add-source .artifacts/tool-e2e \ - --version "$tool_version" - - ./.artifacts/tool-e2e/path/dotnet-funge --help - - output=$(./.artifacts/tool-e2e/path/dotnet-funge "samples/Generator.UseConsole/Programs/hello.b98") - echo "$output" - test "$output" = "Hello, World!" - - dotnet tool uninstall dotnet-funge --tool-path .artifacts/tool-e2e/path - - - name: Pack - if: ${{ matrix.os == 'ubuntu-latest' }} - run: | - dotnet pack -o artifacts/ - - - uses: actions/upload-artifact@v4 - if: ${{ matrix.os == 'ubuntu-latest' }} - with: - name: artifacts - path: artifacts + - uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y clang zlib1g-dev + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore + + - name: Test + run: dotnet test --no-build --verbosity normal + + - name: Tool E2E (pack/install/run) + shell: bash + run: | + set -euo pipefail + rid="${{ matrix.rid }}" + rm -rf .artifacts/tool-e2e + mkdir -p .artifacts/tool-e2e + + dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -r "$rid" -p:ToolPackageRuntimeIdentifiers= -o .artifacts/tool-e2e + + nupkg_path=$(find .artifacts/tool-e2e -maxdepth 1 -type f -name "dotnet-funge.*.nupkg" ! -name "*.snupkg" | head -n 1) + if [ -z "$nupkg_path" ]; then + echo "Failed to find dotnet-funge nupkg in .artifacts/tool-e2e" + exit 1 + fi + nupkg_name=$(basename "$nupkg_path") + tool_version=${nupkg_name#dotnet-funge.} + tool_version=${tool_version%.nupkg} + echo "Detected tool version: $tool_version" + + dotnet new tool-manifest --force + dotnet tool install dotnet-funge \ + --add-source .artifacts/tool-e2e \ + --version "$tool_version" + + dotnet tool run dotnet-funge -- --help + + # hello.b98 ファイルで実行 → "Hello, World!" を検証 + output1=$(dotnet tool run dotnet-funge -- --path samples/Generator.UseConsole/Programs/hello.b98) + echo "$output1" + test "$output1" = "Hello, World!" + + # inline funge テキストで実行 + output2=$(dotnet tool run dotnet-funge -- --source "64+\"!dlroW ,olleH\">:#,_@") + echo "$output2" + test "$output2" = "Hello, World!" + + # 改行を含む inline funge テキストで実行 + inline_source=$(cat <<'EOF' +v +>25*"!dlroW ,olleH",,,,@ +EOF +) + output3=$(dotnet tool run dotnet-funge -- --source "$inline_source") + echo "$output3" + + dotnet tool uninstall dotnet-funge + + - name: Pack + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + dotnet pack Generator/Esolang.Funge.Generator.csproj -o artifacts/ + dotnet pack Parser/Esolang.Funge.Parser.csproj -o artifacts/ + dotnet pack Processor/Esolang.Funge.Processor.csproj -o artifacts/ + dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -p:ToolPackageRuntimeIdentifiers= -o artifacts/ + dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -p:PublishAot=false -p:ToolPackageRuntimeIdentifiers="" -o artifacts/ + + - uses: actions/upload-artifact@v7 + if: ${{ matrix.os == 'ubuntu-latest' }} + with: + name: artifacts + path: artifacts/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 829dbe1..8a0ae07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,33 +17,90 @@ permissions: env: DOTNET_VERSION: '10.0.x' - NUGET_SOURCE: 'https://api.nuget.org/v3/index.json' jobs: + build-aot: + name: build-aot-${{ matrix.rid }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: windows-latest + rid: win-x64 + - os: ubuntu-latest + rid: linux-x64 + - os: macos-latest + rid: osx-x64 + - os: macos-latest + rid: osx-arm64 + - os: ubuntu-latest + rid: any + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' && matrix.rid != 'any' + run: | + sudo apt-get update + sudo apt-get install -y clang zlib1g-dev + + - name: Pack + run: | + dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -r ${{ matrix.rid }} -c Release -o artifacts/ + + - name: Upload Artifact + uses: actions/upload-artifact@v7 + with: + name: nupkg-${{ matrix.rid }} + path: artifacts/* + publish: name: publish-packages-and-release + needs: build-aot runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} fetch-depth: 0 + - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download AOT Artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/aot + pattern: 'nupkg-*' + merge-multiple: true + - name: Restore - run: dotnet restore --source "${{ env.NUGET_SOURCE }}" + run: dotnet restore + - name: Build run: dotnet build --configuration Release --no-restore + - name: Test run: dotnet test --configuration Release --no-build --verbosity normal - - name: Pack + + - name: Pack Libraries run: | - dotnet pack Generator/Esolang.Funge.Generator.csproj -c Release -o artifacts/nuget - dotnet pack Parser/Esolang.Funge.Parser.csproj -c Release -o artifacts/nuget - dotnet pack Processor/Esolang.Funge.Processor.csproj -c Release -o artifacts/nuget - dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -c Release -o artifacts/nuget + dotnet pack Interpreter/Esolang.Funge.Interpreter.csproj -c Release -o artifacts/aot --no-build -p:ToolPackageRuntimeIdentifiers="" + dotnet pack Generator/Esolang.Funge.Generator.csproj -c Release -o artifacts/nuget --no-build + dotnet pack Parser/Esolang.Funge.Parser.csproj -c Release -o artifacts/nuget --no-build + dotnet pack Processor/Esolang.Funge.Processor.csproj -c Release -o artifacts/nuget --no-build + - name: Publish to NuGet.org env: NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} @@ -55,23 +112,48 @@ jobs: exit 1 fi shopt -s nullglob + push_nupkg() { + local pkg="$1" + dotnet nuget push "$pkg" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate + } + push_snupkg() { + local pkg="$1" + local snupkg="${pkg%.nupkg}.snupkg" + if [ -f "$snupkg" ]; then + dotnet nuget push "$snupkg" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate + fi + } + # Collect nupkg files in order: + # 1. RID-specific AOT packages (win-x64, linux-x64, osx-*, etc.) + # 2. any/portable package + # 3. no-RID meta package (the main dotnet-funge tool entry point) + # 4. Library packages (Generator, Parser, Processor) + ordered_nupkgs=() + for pkg in artifacts/aot/*.nupkg; do + [[ "$pkg" =~ \.(win|linux|osx|freebsd|android|ios)- ]] || continue + ordered_nupkgs+=("$pkg") + done + for pkg in artifacts/aot/*.nupkg; do + [[ "$pkg" == *".any."* ]] || continue + ordered_nupkgs+=("$pkg") + done + for pkg in artifacts/aot/*.nupkg; do + [[ "$pkg" =~ \.(win|linux|osx|freebsd|android|ios)- ]] && continue + [[ "$pkg" == *".any."* ]] && continue + ordered_nupkgs+=("$pkg") + done for pkg in artifacts/nuget/*.nupkg; do - case "$pkg" in - *.snupkg) - continue - ;; - esac - dotnet nuget push "$pkg" \ - --api-key "$NUGET_API_KEY" \ - --source https://api.nuget.org/v3/index.json \ - --skip-duplicate + ordered_nupkgs+=("$pkg") + done + # Upload all nupkg files first + for pkg in "${ordered_nupkgs[@]}"; do + push_nupkg "$pkg" done - for symbol_pkg in artifacts/nuget/*.snupkg; do - dotnet nuget push "$symbol_pkg" \ - --api-key "$NUGET_API_KEY" \ - --source https://api.nuget.org/v3/index.json \ - --skip-duplicate + # Then upload all snupkg files + for pkg in "${ordered_nupkgs[@]}"; do + push_snupkg "$pkg" done + - name: Publish to GitHub Packages env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -79,17 +161,13 @@ jobs: run: | set -euo pipefail shopt -s nullglob - for pkg in artifacts/nuget/*.nupkg; do + for pkg in artifacts/aot/*.nupkg artifacts/nuget/*.nupkg; do case "$pkg" in - *.snupkg) - continue - ;; + *.snupkg) continue ;; esac - dotnet nuget push "$pkg" \ - --api-key "$GITHUB_TOKEN" \ - --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ - --skip-duplicate + dotnet nuget push "$pkg" --api-key "$GITHUB_TOKEN" --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" --skip-duplicate done + - name: Create GitHub Release and Upload Assets env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -98,18 +176,33 @@ jobs: set -euo pipefail tag="${{ github.event.inputs.tag || github.ref_name }}" shopt -s nullglob - assets=(artifacts/nuget/*.nupkg artifacts/nuget/*.snupkg) + + lib_assets=(artifacts/nuget/*.nupkg artifacts/nuget/*.snupkg) + + aot_all=(artifacts/aot/*.nupkg artifacts/aot/*.snupkg) + aot_others=() + aot_any=() + for f in "${aot_all[@]}"; do + if [[ "$f" == *".any."* ]]; then + aot_any+=("$f") + else + aot_others+=("$f") + fi + done + + # Order: native RID packages → library packages → any/portable package + upload_files=("${aot_others[@]}" "${lib_assets[@]}" "${aot_any[@]}") + prerelease_flag="" if [[ "$tag" == *-* ]]; then prerelease_flag="--prerelease" fi + if gh release view "$tag" >/dev/null 2>&1; then - if [ ${#assets[@]} -gt 0 ]; then - gh release upload "$tag" "${assets[@]}" --clobber - fi + gh release upload "$tag" "${upload_files[@]}" --clobber else gh release create "$tag" \ - "${assets[@]}" \ + "${upload_files[@]}" \ --title "$tag" \ --generate-notes \ $prerelease_flag diff --git a/CHANGELOG.md b/CHANGELOG.md index 435ef60..594db47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on Keep a Changelog. ## [Unreleased] +## [2.0.0] - 2026-06-03 + +### Added + +- `Esolang.Funge.Interpreter`: added `--path` / `-p` for file input and `--source` / `-s` for inline Funge-98 source, including multiline source text. +- `.github/workflows/dotnet.yml`: updated the CI tool E2E step to exercise the interpreter's new inline-source execution path. +- `Esolang.Funge.Generator`: documented the argument-binding heuristics for `string[]` / `IEnumerable` parameters, including the `arg` / `env` name conventions used for `y`. +- `Esolang.Funge.Generator`: Added FG0011 diagnostic for enforcing partial method declaration. + ## [1.1.1] - 2026-05-25 - `Esolang.Funge.Generator`: Implement logging support for runtime instructions and events (instruction execution, fingerprint operations). @@ -93,7 +102,8 @@ The format is based on Keep a Changelog. - `Esolang.Funge.Generator`: `FG0008` / `FG0009` severity changed from Warning to Info. - `Esolang.Funge.Generator`: runtime now throws when input/output instructions are executed without a declared input/output interface. -[Unreleased]: https://github.com/Esolang-NET/Funge/compare/v1.1.1...HEAD +[Unreleased]: https://github.com/Esolang-NET/Funge/compare/v2.0.0...HEAD +[2.0.0]: https://github.com/Esolang-NET/Funge/compare/v1.1.1...v2.0.0 [1.1.1]: https://github.com/Esolang-NET/Funge/tree/v1.1.1 [1.1.0]: https://github.com/Esolang-NET/Funge/tree/v1.1.0 [1.0.1]: https://github.com/Esolang-NET/Funge/tree/v1.0.1 diff --git a/Directory.Build.props b/Directory.Build.props index cf6dcc5..0dce95b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,9 +3,9 @@ enable enable 14 - 1.1.1.4 - 1.1.1.4 - 1.1.1 + 2.0.0.0 + 2.0.0.5 + 2.0.0 https://github.com/Esolang-NET/Funge/ https://github.com/Esolang-NET/Funge.git true @@ -16,6 +16,7 @@ $(NoWarn);NETSDK1213;CS9057 snupkg True + true @@ -41,8 +42,9 @@ false true false - $(NoWarn);RS1035 + $(NoWarn);RS1035;CS1591 Exe true + true diff --git a/Directory.Build.targets b/Directory.Build.targets index 222a772..b738cef 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -4,8 +4,9 @@ - - + + + @@ -14,7 +15,9 @@ + + diff --git a/Generator.Tests/Esolang.Funge.Generator.Tests.csproj b/Generator.Tests/Esolang.Funge.Generator.Tests.csproj index ab9420a..12b8adb 100644 --- a/Generator.Tests/Esolang.Funge.Generator.Tests.csproj +++ b/Generator.Tests/Esolang.Funge.Generator.Tests.csproj @@ -3,12 +3,6 @@ net48;net8.0;net9.0;net10.0 net8.0;net9.0;net10.0 - enable - enable - - false - true - false Esolang.Funge.Generator.Tests diff --git a/Generator.Tests/FungeMethodGeneratorTests.cs b/Generator.Tests/FungeMethodGeneratorTests.cs index 5704b1d..5df554e 100644 --- a/Generator.Tests/FungeMethodGeneratorTests.cs +++ b/Generator.Tests/FungeMethodGeneratorTests.cs @@ -10,7 +10,7 @@ namespace Esolang.Funge.Generator.Tests; [TestClass] -public class FungeMethodGeneratorTests(TestContext TestContext) +public class FungeMethodGeneratorTests { void LogWriteLine(string message) => TestContext.WriteLine(message); @@ -18,11 +18,13 @@ public class FungeMethodGeneratorTests(TestContext TestContext) CancellationToken CancellationToken => TestContext.CancellationTokenSource.Token; #pragma warning restore MSTEST0054 // TestContext.CancellationTokenSource.Token の代わりに TestContext.CancellationToken を使用する - Compilation baseCompilation = default!; + readonly Compilation baseCompilation = default!; - [TestInitialize] - public void InitializeCompilation() + readonly TestContext TestContext; + + public FungeMethodGeneratorTests(TestContext TestContext) { + this.TestContext = TestContext; IEnumerable references = #if NET10_0_OR_GREATER Net100.References.All; @@ -199,8 +201,8 @@ await Task.Factory.StartNew(() => var logs = (List)logger.Logs; // Check if we have logs for '1' and '@' - Assert.IsTrue(logs.Any(l => l.Contains("'1'"))); - Assert.IsTrue(logs.Any(l => l.Contains("'@'"))); + Assert.Contains(l => l.Contains("'1'"), logs); + Assert.Contains(l => l.Contains("'@'"), logs); }, CancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } catch (Exception e) when (e is AssertFailedException or TargetInvocationException) @@ -259,8 +261,8 @@ await Task.Factory.StartNew(() => var logs = (List)loggerType.GetField("Logs")!.GetValue(loggerInstance)!; - Assert.IsTrue(logs.Any(l => l.Contains("'1'"))); - Assert.IsTrue(logs.Any(l => l.Contains("'@'"))); + Assert.Contains(l => l.Contains("'1'"), logs); + Assert.Contains(l => l.Contains("'@'"), logs); }, CancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } catch (Exception e) when (e is AssertFailedException or TargetInvocationException) @@ -456,7 +458,7 @@ partial class TestClass var expectedFiles = new[] { "input.cs", "GenerateFungeMethodAttribute.cs", "GenerateFungeMethod.g.cs" }; foreach (var expected in expectedFiles) { - Assert.IsTrue(actualPaths.Any(p => p.Contains(expected, StringComparison.OrdinalIgnoreCase)), $"Missing file: {expected}"); + Assert.Contains(p => p.Contains(expected, StringComparison.OrdinalIgnoreCase), actualPaths, $"Missing file: {expected}"); } } catch (Exception e) when (e is AssertFailedException or TargetInvocationException) @@ -1549,7 +1551,7 @@ public async Task Runtime_FileInput_LoadsIntoSpace() Directory.SetCurrentDirectory(tempDir); File.WriteAllText("input.txt", "A"); - var reversed = new string("input.txt".Reverse().ToArray()); + var reversed = new string([.. "input.txt".Reverse()]); var program = $"00000\"{reversed}\"in000gq"; var source = """ @@ -1602,7 +1604,7 @@ public async Task Runtime_FileOutput_WritesRegion() { Directory.SetCurrentDirectory(tempDir); - var reversed = new string("output.txt".Reverse().ToArray()); + var reversed = new string([.. "output.txt".Reverse()]); var program = $"88*1+000p00000000\"{reversed}\"o@"; var source = """ @@ -1650,7 +1652,7 @@ await Task.Factory.StartNew(() => public async Task Runtime_SystemExec_ReturnsExitCode() { const string command = "exit 7"; - var reversed = new string(command.Reverse().ToArray()); + var reversed = new string([.. command.Reverse()]); var program = $"0\"{reversed}\"=q"; var source = """ @@ -1690,7 +1692,7 @@ await Task.Factory.StartNew(() => public async Task Runtime_SystemExec_FailureIsNonZero() { const string command = "this_command_should_not_exist_12345"; - var reversed = new string(command.Reverse().ToArray()); + var reversed = new string([.. command.Reverse()]); var program = $"0\"{reversed}\"=q"; var source = """ @@ -2331,7 +2333,7 @@ partial class TestClass foreach (var tree in comp.SyntaxTrees) { LogWriteLine($"\n--- {tree.FilePath} ---"); - LogWriteLine(tree.GetText().ToString()); + LogWriteLine(tree.GetText(TestContext.CancellationToken).ToString()); } } catch (Exception e) when (e is AssertFailedException or TargetInvocationException) diff --git a/Generator.Tests/PartialMethodConstraintTests.cs b/Generator.Tests/PartialMethodConstraintTests.cs new file mode 100644 index 0000000..3899c58 --- /dev/null +++ b/Generator.Tests/PartialMethodConstraintTests.cs @@ -0,0 +1,36 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Esolang.Funge.Generator.Tests; + +[TestClass] +public class PartialMethodConstraintTests(TestContext TestContext) +{ + +#pragma warning disable MSTEST0054 // TestContext.CancellationTokenSource.Token の代わりに TestContext.CancellationToken を使用する + CancellationToken Cancellationtoken => TestContext.CancellationTokenSource.Token; +#pragma warning restore MSTEST0054 // TestContext.CancellationTokenSource.Token の代わりに TestContext.CancellationToken を使用する + [TestMethod] + public void Generator_NonPartialMethod_ReportsError() + { + const string source = """ + namespace Demo; + + public class Sample + { + [Esolang.Funge.GenerateFungeMethod(InlineSource = ">@")] + public void RunSync() { } + } + """; + + var compilation = CSharpCompilation.Create("Test", + [CSharpSyntaxTree.ParseText(source, cancellationToken: Cancellationtoken)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + + var generator = new MethodGenerator(); + var driver = CSharpGeneratorDriver.Create(generator); + driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out var diagnostics, Cancellationtoken); + + Assert.Contains(d => d.Id == "FG0011", diagnostics, "Expected diagnostic FG0011 (Method must be partial)"); + } +} diff --git a/Generator/AnalyzerReleases.Shipped.md b/Generator/AnalyzerReleases.Shipped.md index dc7074a..6b5e4a9 100644 --- a/Generator/AnalyzerReleases.Shipped.md +++ b/Generator/AnalyzerReleases.Shipped.md @@ -23,3 +23,11 @@ Rule ID | New Category | New Severity | Old Category | Old Severity | Notes --------|--------------|--------------|--------------|--------------|-------------------- FG0008 | Funge | Info | Funge | Warning | Static best-effort diagnostic; runtime throws if reached without output interface FG0009 | Funge | Info | Funge | Warning | Static best-effort diagnostic; runtime throws if reached without input interface + +## Release 2.0.0.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +FG0011 | Funge | Error | Method must be partial diff --git a/Generator/AnalyzerReleases.Unshipped.md b/Generator/AnalyzerReleases.Unshipped.md index 6a2a74b..3287c38 100644 --- a/Generator/AnalyzerReleases.Unshipped.md +++ b/Generator/AnalyzerReleases.Unshipped.md @@ -1,4 +1,4 @@ ### New Rules Rule ID | Category | Severity | Notes ---------|----------|----------|-------------------- +--------|----------|----------|--------- \ No newline at end of file diff --git a/Generator/DiagnosticDescriptors.cs b/Generator/DiagnosticDescriptors.cs index 47905d6..079d55c 100644 --- a/Generator/DiagnosticDescriptors.cs +++ b/Generator/DiagnosticDescriptors.cs @@ -118,4 +118,15 @@ public static class DiagnosticDescriptors category: Category, defaultSeverity: DiagnosticSeverity.Hidden, isEnabledByDefault: true); + + /// + /// FG0011: Method must be partial. + /// + public static readonly DiagnosticDescriptor MethodMustBePartial = new( + id: "FG0011", + title: "Method must be partial", + messageFormat: "The method '{0}' must be declared as 'partial'", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); } diff --git a/Generator/Esolang.Funge.Generator.csproj b/Generator/Esolang.Funge.Generator.csproj index 794d4c9..62dd6b9 100644 --- a/Generator/Esolang.Funge.Generator.csproj +++ b/Generator/Esolang.Funge.Generator.csproj @@ -18,6 +18,11 @@ + + + all + runtime; build; native; contentfiles; analyzers + @@ -25,6 +30,7 @@ + @@ -38,6 +44,8 @@ + + diff --git a/Generator/MethodGenerator.Runtime.cs b/Generator/MethodGenerator.Runtime.cs index 3e2881b..b0dd5b6 100644 --- a/Generator/MethodGenerator.Runtime.cs +++ b/Generator/MethodGenerator.Runtime.cs @@ -9,16 +9,16 @@ partial class MethodGenerator [Flags] enum RuntimeFacadeFeatures { - None = 0, - RunSync = 1 << 0, - RunString = 1 << 1, - RunEnumerable = 1 << 2, + None = 0, + RunSync = 1 << 0, + RunString = 1 << 1, + RunEnumerable = 1 << 2, RunAsyncEnumerable = 1 << 3, - RunTask = 1 << 4, - RunTaskInt = 1 << 5, - RunTaskString = 1 << 6, + RunTask = 1 << 4, + RunTaskInt = 1 << 5, + RunTaskString = 1 << 6, RunValueTask = 1 << 7, - RunValueTaskInt = 1 << 8, + RunValueTaskInt = 1 << 8, RunValueTaskString = 1 << 9, RunWithLogging = 1 << 10, } diff --git a/Generator/MethodGenerator.cs b/Generator/MethodGenerator.cs index 1029744..e129fea 100644 --- a/Generator/MethodGenerator.cs +++ b/Generator/MethodGenerator.cs @@ -1,8 +1,10 @@ using Esolang.Funge.Parser; +using Esolang.Generator; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Text; +using static Esolang.Generator.BindingError; namespace Esolang.Funge.Generator; @@ -32,129 +34,14 @@ public sealed partial class MethodGenerator : IIncrementalGenerator """; - // ----------------------------------------------------------------------- - // Enumerations for execution signature binding - // ----------------------------------------------------------------------- - - enum ReturnKind + readonly record struct FungeExecutionBinding( + MethodSignatureBinding Binding, + string? ArgsExpression = null, + string? EnvsExpression = null, + BindingError? FungeError = null) { - Void, - Int, - String, - Task, - TaskInt, - TaskString, - ValueTask, - ValueTaskInt, - ValueTaskString, - EnumerableByte, - AsyncEnumerableByte, - Invalid, - } - - enum InputKind { None, String, TextReader, PipeReader } - - enum OutputKind { None, TextWriter, PipeWriter, ReturnString, ReturnEnumerable, ReturnAsyncEnumerable } - - readonly struct KnownTypes - { - public readonly INamedTypeSymbol? String; - public readonly INamedTypeSymbol? Task; - public readonly INamedTypeSymbol? TaskInt; - public readonly INamedTypeSymbol? TaskString; - public readonly INamedTypeSymbol? ValueTask; - public readonly INamedTypeSymbol? ValueTaskInt; - public readonly INamedTypeSymbol? ValueTaskString; - public readonly INamedTypeSymbol? IEnumerableByte; - public readonly INamedTypeSymbol? IAsyncEnumerableByte; - public readonly INamedTypeSymbol? Byte; - public readonly INamedTypeSymbol? Int; - public readonly INamedTypeSymbol? TextReader; - public readonly INamedTypeSymbol? PipeReader; - public readonly INamedTypeSymbol? TextWriter; - public readonly INamedTypeSymbol? PipeWriter; - public readonly INamedTypeSymbol? CancellationToken; - public readonly INamedTypeSymbol? ILogger; - public readonly INamedTypeSymbol? ILoggerOfT; - - public KnownTypes(Compilation compilation) - { - String = compilation.GetSpecialType(SpecialType.System_String); - var byteSymbol = compilation.GetSpecialType(SpecialType.System_Byte); - var intSymbol = compilation.GetSpecialType(SpecialType.System_Int32); - - var taskGeneric = GetBestTypeByMetadataName(compilation, "System.Threading.Tasks.Task`1"); - Task = GetBestTypeByMetadataName(compilation, "System.Threading.Tasks.Task"); - TaskInt = taskGeneric?.Construct(intSymbol); - TaskString = taskGeneric?.Construct(String); - - var valueTaskGeneric = GetBestTypeByMetadataName(compilation, "System.Threading.Tasks.ValueTask`1"); - ValueTask = GetBestTypeByMetadataName(compilation, "System.Threading.Tasks.ValueTask"); - ValueTaskInt = valueTaskGeneric?.Construct(intSymbol); - ValueTaskString = valueTaskGeneric?.Construct(String); - - var enumerableGeneric = GetBestTypeByMetadataName(compilation, "System.Collections.Generic.IEnumerable`1"); - IEnumerableByte = enumerableGeneric?.Construct(byteSymbol); - - var asyncEnumerableGeneric = GetBestTypeByMetadataName(compilation, "System.Collections.Generic.IAsyncEnumerable`1"); - IAsyncEnumerableByte = asyncEnumerableGeneric?.Construct(byteSymbol); - - Byte = byteSymbol; - Int = intSymbol; - TextReader = GetBestTypeByMetadataName(compilation, "System.IO.TextReader"); - PipeReader = GetBestTypeByMetadataName(compilation, "System.IO.Pipelines.PipeReader"); - TextWriter = GetBestTypeByMetadataName(compilation, "System.IO.TextWriter"); - PipeWriter = GetBestTypeByMetadataName(compilation, "System.IO.Pipelines.PipeWriter"); - CancellationToken = GetBestTypeByMetadataName(compilation, "System.Threading.CancellationToken"); - ILogger = GetBestTypeByMetadataName(compilation, "Microsoft.Extensions.Logging.ILogger"); - ILoggerOfT = GetBestTypeByMetadataName(compilation, "Microsoft.Extensions.Logging.ILogger`1"); - } - - private static INamedTypeSymbol? GetBestTypeByMetadataName(Compilation compilation, string metadataName) - { - var type = compilation.GetTypeByMetadataName(metadataName); - if (type != null) return type; - - // Manual search through references if the standard lookup fails due to ambiguity - foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols) - { - var found = assembly.GetTypeByMetadataName(metadataName); - if (found != null) return found; - } - return null; - } - } - - readonly struct ExecutionBinding( - bool isValid, - ReturnKind returnKind, - InputKind inputKind, - OutputKind outputKind, - string inputExpression, - string outputExpression, - string? argsExpression, - string? envsExpression, - string? cancellationTokenName, - string? loggerExpression, - bool isLoggerFromParameter, - string? errorId, - Location? location = null) - { - public bool IsValid { get; } = isValid; - public ReturnKind ReturnKind { get; } = returnKind; - public InputKind InputKind { get; } = inputKind; - public OutputKind OutputKind { get; } = outputKind; - public string InputExpression { get; } = inputExpression; - public string OutputExpression { get; } = outputExpression; - public string? ArgsExpression { get; } = argsExpression; - public string? EnvsExpression { get; } = envsExpression; - public string? CancellationTokenName { get; } = cancellationTokenName; - public string? LoggerExpression { get; } = loggerExpression; - public bool IsLoggerFromParameter { get; } = isLoggerFromParameter; - public string? ErrorId { get; } = errorId; - public Location? Location { get; } = location; - public bool HasExplicitInput => InputKind is not InputKind.None; - public bool HasExplicitOutput => OutputKind is not OutputKind.None; + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, nameof(FungeError))] + public bool IsValid => Binding.IsValid && FungeError is null; } // ----------------------------------------------------------------------- @@ -235,6 +122,15 @@ internal sealed class {{AttributeName}} : Attribute var method = (MethodDeclarationSyntax)syntaxCtx.TargetNode; var symbol = (IMethodSymbol)syntaxCtx.TargetSymbol; + if (!symbol.IsPartialDefinition) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.MethodMustBePartial, + method.Identifier.GetLocation(), + symbol.Name)); + continue; + } + if (!IsLanguageVersionAtLeastCSharp8(langVersion)) { ctx.ReportDiagnostic(Diagnostic.Create( @@ -276,20 +172,20 @@ internal sealed class {{AttributeName}} : Attribute break; } } - + if (inlineSource == null) { - var attributeSyntax = attrData.ApplicationSyntaxReference?.GetSyntax() as AttributeSyntax; - if (attributeSyntax?.ArgumentList != null) - { - foreach (var arg in attributeSyntax.ArgumentList.Arguments) - { - if (arg.NameEquals?.Name.Identifier.ValueText == "InlineSource" && arg.Expression is LiteralExpressionSyntax lit) - { - inlineSource = lit.Token.ValueText; - } - } - } + var attributeSyntax = attrData.ApplicationSyntaxReference?.GetSyntax() as AttributeSyntax; + if (attributeSyntax?.ArgumentList != null) + { + foreach (var arg in attributeSyntax.ArgumentList.Arguments) + { + if (arg.NameEquals?.Name.Identifier.ValueText == "InlineSource" && arg.Expression is LiteralExpressionSyntax lit) + { + inlineSource = lit.Token.ValueText; + } + } + } } if (string.IsNullOrWhiteSpace(inlineSource) && string.IsNullOrWhiteSpace(sourcePath)) @@ -305,22 +201,30 @@ internal sealed class {{AttributeName}} : Attribute var binding = BindExecutionSignature(symbol, method, types); if (!binding.IsValid) { - if (binding.ErrorId == DiagnosticDescriptors.InvalidReturnType.Id) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidReturnType, - binding.Location ?? method.Identifier.GetLocation(), - symbol.ReturnType.ToDisplayString())); - else if (binding.ErrorId == DiagnosticDescriptors.DuplicateParameter.Id) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.DuplicateParameter, - binding.Location ?? method.Identifier.GetLocation(), - symbol.Name, symbol.Name)); - else if (binding.ErrorId == DiagnosticDescriptors.ReturnOutputConflict.Id) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ReturnOutputConflict, - binding.Location ?? method.Identifier.GetLocation(), - symbol.Name)); - else - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidParameter, - binding.Location ?? method.Identifier.GetLocation(), - symbol.Name)); + var error = binding.FungeError ?? binding.Binding.Error!; + var location = error.Location ?? method.Identifier.GetLocation(); + + var descriptor = error switch + { + UnsupportedReturnType => DiagnosticDescriptors.InvalidReturnType, + DuplicateInput or DuplicateOutput or DuplicateCancellationToken or DuplicateLogger => DiagnosticDescriptors.DuplicateParameter, + ReturnOutputConflict => DiagnosticDescriptors.ReturnOutputConflict, + _ => DiagnosticDescriptors.InvalidParameter, + }; + + var messageArgs = error switch + { + UnsupportedReturnType e => new object[] { e.ReturnType.ToDisplayString() }, + DuplicateInput e => [e.Parameter.Type.ToDisplayString(), symbol.Name], + DuplicateOutput e => [e.Parameter.Type.ToDisplayString(), symbol.Name], + DuplicateCancellationToken e => [e.Parameter.Type.ToDisplayString(), symbol.Name], + DuplicateLogger e => [e.Parameter.Type.ToDisplayString(), symbol.Name], + ReturnOutputConflict e => [symbol.Name], + InvalidParameterModifier e => [e.Parameter.Name], + _ => [symbol.Name] + }; + + ctx.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs)); continue; } @@ -340,7 +244,7 @@ internal sealed class {{AttributeName}} : Attribute // Strip the ".funge.txt" intermediate suffix that the .targets file appends const string fungeSuffix = ".funge.txt"; var compareFile = normalizedFile.EndsWith(fungeSuffix, StringComparison.OrdinalIgnoreCase) - ? normalizedFile.Substring(0, normalizedFile.Length - fungeSuffix.Length) + ? normalizedFile[..^fungeSuffix.Length] : normalizedFile; if (string.Equals(compareFile, normalizedSource, StringComparison.OrdinalIgnoreCase) || compareFile.EndsWith("/" + normalizedSource, StringComparison.OrdinalIgnoreCase) @@ -368,15 +272,15 @@ internal sealed class {{AttributeName}} : Attribute // Scan for I/O usage var (usesOutput, usesInput) = ScanFungeIo(space); - if (usesOutput && !binding.HasExplicitOutput) + if (usesOutput && !binding.Binding.HasExplicitOutput) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.RequiredOutputInterface, method.Identifier.GetLocation(), symbol.Name)); - if (usesInput && !binding.HasExplicitInput) + if (usesInput && !binding.Binding.HasExplicitInput) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.RequiredInputInterface, method.Identifier.GetLocation(), symbol.Name)); - if (!usesInput && binding.HasExplicitInput) + if (!usesInput && binding.Binding.HasExplicitInput) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UnusedInputInterface, method.Identifier.GetLocation(), symbol.Name)); @@ -384,8 +288,8 @@ internal sealed class {{AttributeName}} : Attribute var emitted = EmitMethod(symbol, method, space, binding, projDir, displayPath); methodSb.AppendLine(emitted); emittedCount++; - runtimeFeatures |= GetRuntimeFacadeFeatures(binding.ReturnKind); - if (binding.LoggerExpression is not null) + runtimeFeatures |= GetRuntimeFacadeFeatures(binding.Binding.ReturnKind); + if (binding.Binding.LoggerExpression is not null) runtimeFeatures |= RuntimeFacadeFeatures.RunWithLogging; } @@ -396,18 +300,18 @@ internal sealed class {{AttributeName}} : Attribute }); } - static RuntimeFacadeFeatures GetRuntimeFacadeFeatures(ReturnKind returnKind) => returnKind switch + static RuntimeFacadeFeatures GetRuntimeFacadeFeatures(MethodReturnKind returnKind) => returnKind switch { - ReturnKind.Void or ReturnKind.Int => RuntimeFacadeFeatures.RunSync, - ReturnKind.String => RuntimeFacadeFeatures.RunString, - ReturnKind.Task => RuntimeFacadeFeatures.RunTask, - ReturnKind.TaskInt => RuntimeFacadeFeatures.RunTaskInt, - ReturnKind.TaskString => RuntimeFacadeFeatures.RunTaskString, - ReturnKind.ValueTask => RuntimeFacadeFeatures.RunValueTask, - ReturnKind.ValueTaskInt => RuntimeFacadeFeatures.RunValueTaskInt, - ReturnKind.ValueTaskString => RuntimeFacadeFeatures.RunValueTaskString, - ReturnKind.EnumerableByte => RuntimeFacadeFeatures.RunEnumerable, - ReturnKind.AsyncEnumerableByte => RuntimeFacadeFeatures.RunAsyncEnumerable, + MethodReturnKind.Void or MethodReturnKind.Int32 => RuntimeFacadeFeatures.RunSync, + MethodReturnKind.String or MethodReturnKind.NullableString => RuntimeFacadeFeatures.RunString, + MethodReturnKind.Task => RuntimeFacadeFeatures.RunTask, + MethodReturnKind.TaskInt32 => RuntimeFacadeFeatures.RunTaskInt, + MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString => RuntimeFacadeFeatures.RunTaskString, + MethodReturnKind.ValueTask => RuntimeFacadeFeatures.RunValueTask, + MethodReturnKind.ValueTaskInt32 => RuntimeFacadeFeatures.RunValueTaskInt, + MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString => RuntimeFacadeFeatures.RunValueTaskString, + MethodReturnKind.IEnumerableByte => RuntimeFacadeFeatures.RunEnumerable, + MethodReturnKind.IAsyncEnumerableByte => RuntimeFacadeFeatures.RunAsyncEnumerable, _ => RuntimeFacadeFeatures.None, }; @@ -415,260 +319,57 @@ internal sealed class {{AttributeName}} : Attribute // Signature binding // ----------------------------------------------------------------------- - static bool IsSameType(ITypeSymbol? type, INamedTypeSymbol? knownType) + static FungeExecutionBinding BindExecutionSignature(IMethodSymbol method, MethodDeclarationSyntax syntax, KnownTypes types) { - if (type is null || knownType is null) return false; - return SymbolEqualityComparer.Default.Equals(type, knownType); - } - - static bool IsSameTypeOrConstructedFrom(ITypeSymbol? type, INamedTypeSymbol? knownType) - { - if (type is null || knownType is null) return false; - if (SymbolEqualityComparer.Default.Equals(type, knownType)) return true; - if (type is INamedTypeSymbol namedType && namedType.IsGenericType && SymbolEqualityComparer.Default.Equals(namedType.ConstructedFrom, knownType)) return true; - return false; - } + var binding = MethodSignatureBinder.Bind(method, types); + if (!binding.IsValid) + return new FungeExecutionBinding(binding); - static bool IsLoggerType(ITypeSymbol? type, KnownTypes types) - { - if (type is null) return false; - if (IsSameType(type, types.ILogger) || IsSameTypeOrConstructedFrom(type, types.ILoggerOfT)) return true; - - foreach (var iface in type.AllInterfaces) - { - if (IsSameType(iface, types.ILogger) || IsSameTypeOrConstructedFrom(iface, types.ILoggerOfT)) return true; - } - return false; - } - - static ExecutionBinding BindExecutionSignature(IMethodSymbol method, MethodDeclarationSyntax syntax, KnownTypes types) - { - var returnType = method.ReturnType; - - var returnKind = ReturnKind.Invalid; - - if (returnType.SpecialType == SpecialType.System_Void) returnKind = ReturnKind.Void; - else if (returnType.SpecialType == SpecialType.System_Int32) returnKind = ReturnKind.Int; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.String)) returnKind = ReturnKind.String; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.Task)) returnKind = ReturnKind.Task; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.TaskInt)) returnKind = ReturnKind.TaskInt; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.TaskString)) returnKind = ReturnKind.TaskString; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.ValueTask)) returnKind = ReturnKind.ValueTask; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.ValueTaskInt)) returnKind = ReturnKind.ValueTaskInt; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.ValueTaskString)) returnKind = ReturnKind.ValueTaskString; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.IEnumerableByte)) returnKind = ReturnKind.EnumerableByte; - else if (SymbolEqualityComparer.Default.Equals(returnType, types.IAsyncEnumerableByte)) returnKind = ReturnKind.AsyncEnumerableByte; - - if (returnKind == ReturnKind.Invalid) - { - // returnKind is invalid, check types for debugging - // Using a diagnostic for debugging as we are in a generator - // ctx.ReportDiagnostic(...); // Need context here, but BindExecutionSignature doesn't have it. - // We'll have to return an error diagnostic later in Initialize. - // For now, let's keep returnKind as invalid to trigger the error. - } - - if (returnKind == ReturnKind.Invalid) - return new(false, returnKind, InputKind.None, OutputKind.None, "", "", null, null, null, null, false, - DiagnosticDescriptors.InvalidReturnType.Id); - - var outputKind = returnKind switch - { - ReturnKind.String or ReturnKind.TaskString or ReturnKind.ValueTaskString - => OutputKind.ReturnString, - ReturnKind.EnumerableByte => OutputKind.ReturnEnumerable, - ReturnKind.AsyncEnumerableByte => OutputKind.ReturnAsyncEnumerable, - _ => OutputKind.None, - }; - - var inputKind = InputKind.None; - var inputExpr = ""; - var outputExpr = ""; string? argsExpr = null; string? envsExpr = null; - string? cancellationTokenName = null; - var hasCancellationToken = false; - - string? loggerExpression = null; - var isLoggerFromParameter = false; - foreach (var p in method.Parameters) + foreach (var p in binding.UnhandledParameters) { - if (p.RefKind is not RefKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, null, null, null, false, - DiagnosticDescriptors.InvalidParameter.Id, p.Locations.FirstOrDefault()); - - var typeName = p.Type.ToDisplayString(); - - if (IsLoggerType(p.Type, types)) + var type = p.Type; + // Funge-specific logic: match string array or IEnumerable for args/envs + var isStringContainer = false; + if (type is IArrayTypeSymbol arrayType && types.IsString(arrayType.ElementType, false)) { - if (loggerExpression is not null) - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, argsExpr, envsExpr, - cancellationTokenName, null, false, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - loggerExpression = p.Name; - isLoggerFromParameter = true; - continue; + isStringContainer = true; } - - if (typeName == "string") + else if (type is INamedTypeSymbol namedType && namedType.IsGenericType) { - if (inputKind is not InputKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, null, null, null, false, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); - inputKind = InputKind.String; - inputExpr = p.Name; - continue; + if (SymbolEqualityComparer.Default.Equals(namedType.ConstructedFrom, types.IEnumerableT) + && types.IsString(namedType.TypeArguments[0], false)) + { + isStringContainer = true; + } } - // String array or IEnumerable - if (p.Type is IArrayTypeSymbol || (p.Type is INamedTypeSymbol namedType && (namedType.Name == "IEnumerable" || namedType.Name == "IEnumerable`1"))) + if (isStringContainer) { - // This is a rough check, keeping existing logic var name = p.Name.ToLowerInvariant(); if (name.Contains("arg")) { if (argsExpr is not null) - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, null, null, null, null, false, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); + return new(binding, FungeError: new DuplicateInput(p, MethodInputKind.None, p.Locations.FirstOrDefault())); argsExpr = p.Name; continue; } if (name.Contains("env")) { if (envsExpr is not null) - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, null, null, null, null, false, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); + return new(binding, FungeError: new DuplicateInput(p, MethodInputKind.None, p.Locations.FirstOrDefault())); envsExpr = p.Name; continue; } } - if (typeName.Contains("TextReader")) - { - if (inputKind is not InputKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, null, null, null, false, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); - inputKind = InputKind.TextReader; - inputExpr = p.Name; - continue; - } - - if (typeName.Contains("PipeReader")) - { - if (inputKind is not InputKind.None) - return new(false, returnKind, inputKind, outputKind, "", "", null, null, null, null, false, - DiagnosticDescriptors.DuplicateParameter.Id, p.Locations.FirstOrDefault()); - inputKind = InputKind.PipeReader; - inputExpr = p.Name; - continue; - } - - if (typeName.Contains("PipeWriter")) - { - if (outputKind is OutputKind.ReturnString or OutputKind.ReturnEnumerable or OutputKind.ReturnAsyncEnumerable) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.ReturnOutputConflict.Id, - p.Locations.FirstOrDefault()); - if (outputKind is not OutputKind.None) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - outputKind = OutputKind.PipeWriter; - outputExpr = p.Name; - continue; - } - - if (typeName.Contains("TextWriter")) - { - if (outputKind is OutputKind.ReturnString or OutputKind.ReturnEnumerable or OutputKind.ReturnAsyncEnumerable) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.ReturnOutputConflict.Id, - p.Locations.FirstOrDefault()); - if (outputKind is not OutputKind.None) - return new(false, returnKind, inputKind, outputKind, inputExpr, p.Name, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - outputKind = OutputKind.TextWriter; - outputExpr = p.Name; - continue; - } - - if (typeName.Contains("CancellationToken")) - { - if (hasCancellationToken) - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.DuplicateParameter.Id, - p.Locations.FirstOrDefault()); - hasCancellationToken = true; - cancellationTokenName = p.Name; - continue; - } - - return new(false, returnKind, inputKind, outputKind, inputExpr, outputExpr, null, null, - cancellationTokenName, null, false, DiagnosticDescriptors.InvalidParameter.Id, - p.Locations.FirstOrDefault()); - } - - var fieldName = FindLoggerField(method.ContainingType, method.IsStatic, types, out var isField); - if (loggerExpression == null) - { - loggerExpression = fieldName; - isLoggerFromParameter = !isField; + // Still unhandled + return new(binding, FungeError: new InvalidParameterModifier(p, p.Locations.FirstOrDefault())); } - return new(true, returnKind, inputKind, outputKind, inputExpr, outputExpr, argsExpr, envsExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, null); - } - - static string? FindLoggerField(ITypeSymbol? type, bool isStatic, KnownTypes types, out bool isField) - { - isField = false; - var isBaseType = false; - var shadowedNames = new System.Collections.Generic.HashSet(System.StringComparer.Ordinal); - var currentType = type; - - while (currentType != null) - { - foreach (var field in currentType.GetMembers().OfType()) - { - if (isStatic && !field.IsStatic) continue; - - // If searching in a base type, the field must be accessible (protected or public) - if (isBaseType && field.DeclaredAccessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal or Accessibility.Public or Accessibility.Internal)) - continue; - - if (IsLoggerType(field.Type, types)) - { - isField = true; - return field.Name; - } - else if (field.CanBeReferencedByName) - { - shadowedNames.Add(field.Name); - } - } - currentType = currentType.BaseType; - isBaseType = true; - } - - if (type is INamedTypeSymbol namedType) - { - foreach (var constructor in namedType.InstanceConstructors) - { - if (constructor.DeclaringSyntaxReferences.Any(ds => ds.GetSyntax() is ClassDeclarationSyntax)) - { - foreach (var parameter in constructor.Parameters) - { - if (IsLoggerType(parameter.Type, types) && !shadowedNames.Contains(parameter.Name)) - { - isField = false; - return parameter.Name; - } - } - } - } - } - return null; + return new(binding, argsExpr, envsExpr); } // ----------------------------------------------------------------------- @@ -679,7 +380,7 @@ static string EmitMethod( IMethodSymbol symbol, MethodDeclarationSyntax syntax, FungeSpace space, - ExecutionBinding binding, + FungeExecutionBinding binding, string? projDir, string sourcePath) { @@ -698,14 +399,14 @@ static string EmitMethod( var typeName = symbol.ContainingType.Name; var accessibility = GetAccessibility(symbol.DeclaredAccessibility); var staticMod = symbol.IsStatic ? " static" : string.Empty; - var asyncMod = binding.ReturnKind == ReturnKind.AsyncEnumerableByte ? " async" : string.Empty; + var asyncMod = binding.Binding.ReturnKind == MethodReturnKind.IAsyncEnumerableByte ? " async" : string.Empty; var returnTypeSyntax = symbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var paramList = string.Join(", ", Enumerable.Select(symbol.Parameters, p => + var paramList = string.Join(", ", symbol.Parameters.Select(p => { - var prefix = (binding.ReturnKind == ReturnKind.AsyncEnumerableByte - && binding.CancellationTokenName is not null - && string.Equals(p.Name, binding.CancellationTokenName, StringComparison.Ordinal)) + var prefix = (binding.Binding.IsAsyncEnumerable + && binding.Binding.CancellationTokenName is not null + && string.Equals(p.Name, binding.Binding.CancellationTokenName, StringComparison.Ordinal)) ? "[global::System.Runtime.CompilerServices.EnumeratorCancellation] " : string.Empty; return prefix + p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + " " + p.Name; @@ -731,21 +432,22 @@ static string EmitMethod( return sb.ToString(); } - static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding binding, bool isStatic) + static void EmitBody(StringBuilder sb, FungeSpace space, FungeExecutionBinding fungeBinding, bool isStatic) { + var binding = fungeBinding.Binding; var inputExpr = binding.InputKind switch { - InputKind.None => "global::System.IO.TextReader.Null", - InputKind.String => $"new global::System.IO.StringReader({binding.InputExpression} ?? string.Empty)", - InputKind.TextReader => binding.InputExpression, - InputKind.PipeReader => $"new global::System.IO.StreamReader({binding.InputExpression}.AsStream())", + MethodInputKind.None => "global::System.IO.TextReader.Null", + MethodInputKind.String => $"new global::System.IO.StringReader({binding.InputExpression} ?? string.Empty)", + MethodInputKind.TextReader => binding.InputExpression, + MethodInputKind.PipeReader => $"new global::System.IO.StreamReader({binding.InputExpression}.AsStream())", _ => "global::System.IO.TextReader.Null", }; var cancellationTokenExpr = binding.CancellationTokenName is null ? "global::System.Threading.CancellationToken.None" : binding.CancellationTokenName; - var argsExpr = binding.ArgsExpression ?? "null"; - var envsExpr = binding.EnvsExpression ?? "null"; + var argsExpr = fungeBinding.ArgsExpression ?? "null"; + var envsExpr = fungeBinding.EnvsExpression ?? "null"; EmitSpaceData(sb, space); @@ -758,9 +460,9 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin switch (binding.ReturnKind) { - case ReturnKind.Void: + case MethodReturnKind.Void: { - if (binding.OutputKind == OutputKind.PipeWriter) + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($""" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true); @@ -770,7 +472,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -781,8 +483,8 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin break; } - case ReturnKind.Int: - if (binding.OutputKind == OutputKind.PipeWriter) + case MethodReturnKind.Int32: + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($""" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true); @@ -792,7 +494,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -802,16 +504,17 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } break; - case ReturnKind.String: + case MethodReturnKind.String: + case MethodReturnKind.NullableString: sb.AppendLine($""" return global::Esolang.Funge.__Generated.FungeRuntime.RunString( __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}, {argsExpr}, {envsExpr}{loggerExpr}); """); break; - case ReturnKind.Task: + case MethodReturnKind.Task: { - if (binding.OutputKind == OutputKind.PipeWriter) + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($""" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true); @@ -821,7 +524,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -832,8 +535,8 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin break; } - case ReturnKind.TaskInt: - if (binding.OutputKind == OutputKind.PipeWriter) + case MethodReturnKind.TaskInt32: + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($$""" return __RunTaskIntWithPipeWriter(); @@ -848,7 +551,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -858,16 +561,17 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } break; - case ReturnKind.TaskString: + case MethodReturnKind.TaskString: + case MethodReturnKind.TaskNullableString: sb.AppendLine($""" return global::Esolang.Funge.__Generated.FungeRuntime.RunTaskString( __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}, {argsExpr}, {envsExpr}{loggerExpr}); """); break; - case ReturnKind.ValueTask: + case MethodReturnKind.ValueTask: { - if (binding.OutputKind == OutputKind.PipeWriter) + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($""" using var __fungeOutput = new global::System.IO.StreamWriter({binding.OutputExpression}.AsStream(), global::System.Text.Encoding.UTF8, 1024, leaveOpen: true); @@ -877,7 +581,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -888,8 +592,8 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin break; } - case ReturnKind.ValueTaskInt: - if (binding.OutputKind == OutputKind.PipeWriter) + case MethodReturnKind.ValueTaskInt32: + if (binding.OutputKind == MethodOutputKind.PipeWriter) { sb.AppendLine($$""" return __RunValueTaskIntWithPipeWriter(); @@ -904,7 +608,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } else { - var outExpr = binding.OutputKind == OutputKind.TextWriter + var outExpr = binding.OutputKind == MethodOutputKind.TextWriter ? binding.OutputExpression : "global::System.IO.TextWriter.Null"; sb.AppendLine($""" @@ -914,14 +618,15 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin } break; - case ReturnKind.ValueTaskString: + case MethodReturnKind.ValueTaskString: + case MethodReturnKind.ValueTaskNullableString: sb.AppendLine($""" return global::Esolang.Funge.__Generated.FungeRuntime.RunValueTaskString( __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}, {argsExpr}, {envsExpr}{loggerExpr}); """); break; - case ReturnKind.EnumerableByte: + case MethodReturnKind.IEnumerableByte: sb.AppendLine($""" foreach (var __b in global::Esolang.Funge.__Generated.FungeRuntime.RunEnumerable( __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}, {argsExpr}, {envsExpr}{loggerExpr})) @@ -929,7 +634,7 @@ static void EmitBody(StringBuilder sb, FungeSpace space, ExecutionBinding bindin """); break; - case ReturnKind.AsyncEnumerableByte: + case MethodReturnKind.IAsyncEnumerableByte: sb.AppendLine($""" await foreach (var __b in global::Esolang.Funge.__Generated.FungeRuntime.RunAsyncEnumerable( __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {(binding.HasExplicitInput ? "true" : "false")}, {(binding.HasExplicitOutput ? "true" : "false")}, {cancellationTokenExpr}, {argsExpr}, {envsExpr}{loggerExpr})) @@ -939,12 +644,6 @@ 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( - __cells, __minX, __minY, __minZ, __maxX, __maxY, __maxZ, {inputExpr}, {outputExpr}, {(hasInput ? "true" : "false")}, {(hasOutput ? "true" : "false")}); - """); - static void EmitSpaceData(StringBuilder sb, FungeSpace space) { sb.AppendLine($""" @@ -1013,7 +712,7 @@ static string MakeRelative(string baseDir, string fullPath) var sep = System.IO.Path.DirectorySeparatorChar.ToString(); if (!baseDir.EndsWith(sep)) baseDir += sep; return fullPath.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase) - ? fullPath.Substring(baseDir.Length) + ? fullPath[baseDir.Length..] : fullPath; } } diff --git a/Generator/README.md b/Generator/README.md index b1b82e5..31e9de7 100644 --- a/Generator/README.md +++ b/Generator/README.md @@ -28,7 +28,7 @@ The generator reads the Funge-98 source (from a file or inline) and emits a comp | Parameter type | Role | | --- | --- | | `string` | Input fed to the program (`&` / `~`) | -| `string[]` or `IEnumerable` | Command-line arguments or environment variables (detected by name: `args`, `envs`, etc.). Reported by `y`. | +| `string[]` or `IEnumerable` | Command-line arguments or environment variables. The generator binds these by both type and name heuristics: parameter names containing `arg` map to command-line arguments, and names containing `env` map to environment variables. Reported by `y`. | | `System.IO.TextReader` | Input reader | | `System.IO.Pipelines.PipeReader` | Input as pipe | | `System.IO.TextWriter` | Explicit output sink for methods that do not return output text/bytes (including `void`, `int`, `Task`, `Task`, `ValueTask`, `ValueTask`) | @@ -64,7 +64,8 @@ partial class MyPrograms [GenerateFungeMethod("Programs/hello.b98")] public static partial string HelloWorld(); - // With custom arguments and environment variables (reported by 'y' instruction) + // With custom arguments and environment variables (reported by 'y' instruction). + // Parameter names matter here: names containing 'arg' bind as args, and names containing 'env' bind as envs. [GenerateFungeMethod("Programs/sysinfo.b98")] public static partial int RunWithArgs(string[] args, string[] envs); @@ -144,16 +145,17 @@ public partial class MyPrograms(ILogger logger) | ID | Severity | Description | | --- | --- | --- | -| FG0001 | Error | `sourcePath` is empty and `InlineSource` is not set | -| FG0002 | Error | Unsupported return type | -| FG0003 | Error | Unsupported parameter type | -| FG0004 | Error | Source file not found in `AdditionalFiles` | -| FG0005 | Warning | C# language version is too low (requires ≥ C# 8) | -| FG0006 | Error | Duplicate input/output parameter | -| FG0007 | Error | Return type conflicts with explicit output parameter | -| FG0008 | Info | Program appears to use output (`.`/`,`) but no output parameter or output return type is declared (static best-effort scan; runtime throws if reached) | -| FG0009 | Info | Program appears to use input (`&`/`~`) but no input parameter is declared (static best-effort scan; runtime throws if reached) | -| FG0010 | Hidden | Input parameter declared but program never reads input | +| [FG0001](Rules/FG0001.md) | Error | `sourcePath` is empty and `InlineSource` is not set | +| [FG0002](Rules/FG0002.md) | Error | Unsupported return type | +| [FG0003](Rules/FG0003.md) | Error | Unsupported parameter type | +| [FG0004](Rules/FG0004.md) | Error | Source file not found in `AdditionalFiles` | +| [FG0005](Rules/FG0005.md) | Warning | C# language version is too low (requires ≥ C# 8) | +| [FG0006](Rules/FG0006.md) | Error | Duplicate input/output parameter | +| [FG0007](Rules/FG0007.md) | Error | Return type conflicts with explicit output parameter | +| [FG0008](Rules/FG0008.md) | Info | Program appears to use output (`.`/`,`) but no output parameter or output return type is declared (static best-effort scan; runtime throws if reached) | +| [FG0009](Rules/FG0009.md) | Info | Program appears to use input (`&`/`~`) but no input parameter is declared (static best-effort scan; runtime throws if reached) | +| [FG0010](Rules/FG0010.md) | Hidden | Input parameter declared but program never reads input | +| [FG0011](Rules/FG0011.md) | Error | Method must be partial | ## Funge-98 Compliance diff --git a/Generator/Rules/FG0001.md b/Generator/Rules/FG0001.md new file mode 100644 index 0000000..1030170 --- /dev/null +++ b/Generator/Rules/FG0001.md @@ -0,0 +1,18 @@ +# FG0001: Invalid source path parameter + +## Cause +The `sourcePath` provided to the `GenerateFungeMethodAttribute` is null or empty. + +## Solution +Ensure a valid, non-empty source path or inline source is provided to the attribute. + +## Example + +```cs +partial class Sample +{ + // Incorrect: Missing or empty source + [GenerateFungeMethod("")] + public static partial void Invalid(); +} +``` diff --git a/Generator/Rules/FG0002.md b/Generator/Rules/FG0002.md new file mode 100644 index 0000000..9abd063 --- /dev/null +++ b/Generator/Rules/FG0002.md @@ -0,0 +1,18 @@ +# FG0002: Unsupported return type + +## Cause +The method's return type is not supported by the Funge source generator. + +## Solution +Change the return type to one of the supported types (e.g., `void`, `int`, `string`, `Task`, `ValueTask`, `IEnumerable`, `IAsyncEnumerable`). + +## Example + +```cs +partial class Sample +{ + // Incorrect: double is not a supported return type + [GenerateFungeMethod(InlineSource = ">@")] + public static partial double Invalid(); +} +``` diff --git a/Generator/Rules/FG0003.md b/Generator/Rules/FG0003.md new file mode 100644 index 0000000..0ce0d43 --- /dev/null +++ b/Generator/Rules/FG0003.md @@ -0,0 +1,18 @@ +# FG0003: Unsupported parameter type + +## Cause +The method contains a parameter of an unsupported type. + +## Solution +Ensure all parameters are of a supported type (e.g., `string`, `TextReader`, `PipeReader`, `TextWriter`, `PipeWriter`, `CancellationToken`, `ILogger`). + +## Example + +```cs +partial class Sample +{ + // Incorrect: int is not a supported parameter type + [GenerateFungeMethod(InlineSource = ">@")] + public static partial void Invalid(int param); +} +``` diff --git a/Generator/Rules/FG0004.md b/Generator/Rules/FG0004.md new file mode 100644 index 0000000..0628a73 --- /dev/null +++ b/Generator/Rules/FG0004.md @@ -0,0 +1,18 @@ +# FG0004: Funge source file not found + +## Cause +The Funge source file path provided to the `GenerateFungeMethodAttribute` cannot be resolved. + +## Solution +Ensure the source file exists and is correctly referenced in the project's `AdditionalFiles`. + +## Example + +```cs +partial class Sample +{ + // Incorrect: "nonexistent.b98" is not found + [GenerateFungeMethod("nonexistent.b98")] + public static partial void Invalid(); +} +``` diff --git a/Generator/Rules/FG0005.md b/Generator/Rules/FG0005.md new file mode 100644 index 0000000..30a623c --- /dev/null +++ b/Generator/Rules/FG0005.md @@ -0,0 +1,7 @@ +# FG0005: Language version too low + +## Cause +The consumer language version is below C# 8.0, which is required for Funge source generation. + +## Solution +Update the C# language version to 8.0 or later. diff --git a/Generator/Rules/FG0006.md b/Generator/Rules/FG0006.md new file mode 100644 index 0000000..512d990 --- /dev/null +++ b/Generator/Rules/FG0006.md @@ -0,0 +1,18 @@ +# FG0006: Duplicate parameter type + +## Cause +The method contains multiple parameters of the same type or conflicting types. + +## Solution +Ensure each supported parameter type is only used once per method. + +## Example + +```cs +partial class Sample +{ + // Incorrect: duplicate CancellationToken parameter + [GenerateFungeMethod(InlineSource = ">@")] + public static partial void Invalid(System.Threading.CancellationToken ct1, System.Threading.CancellationToken ct2); +} +``` diff --git a/Generator/Rules/FG0007.md b/Generator/Rules/FG0007.md new file mode 100644 index 0000000..67a78c6 --- /dev/null +++ b/Generator/Rules/FG0007.md @@ -0,0 +1,18 @@ +# FG0007: Return type and output parameter conflict + +## Cause +Method has both return-based output and an output parameter (TextWriter/PipeWriter). + +## Solution +Use one or the other, not both. + +## Example + +```cs +partial class Sample +{ + // Incorrect: string return and TextWriter output parameter conflict + [GenerateFungeMethod(InlineSource = ".@")] + public static partial string Invalid(System.IO.TextWriter output); +} +``` diff --git a/Generator/Rules/FG0008.md b/Generator/Rules/FG0008.md new file mode 100644 index 0000000..f63b7b8 --- /dev/null +++ b/Generator/Rules/FG0008.md @@ -0,0 +1,18 @@ +# FG0008: Output interface required + +## Cause +Method uses Funge output instructions but has no output mechanism defined. + +## Solution +Add a return type (string/IEnumerable<byte>) or a TextWriter/PipeWriter parameter. + +## Example + +```cs +partial class Sample +{ + // Incorrect: requires output but no output mechanism + [GenerateFungeMethod(InlineSource = ".@")] + public static partial void Invalid(); +} +``` diff --git a/Generator/Rules/FG0009.md b/Generator/Rules/FG0009.md new file mode 100644 index 0000000..e940881 --- /dev/null +++ b/Generator/Rules/FG0009.md @@ -0,0 +1,18 @@ +# FG0009: Input interface required + +## Cause +Method uses Funge input instructions but has no input mechanism defined. + +## Solution +Add an input parameter (string, TextReader, or PipeReader). + +## Example + +```cs +partial class Sample +{ + // Incorrect: requires input but no input mechanism + [GenerateFungeMethod(InlineSource = ",@")] + public static partial void Invalid(); +} +``` diff --git a/Generator/Rules/FG0010.md b/Generator/Rules/FG0010.md new file mode 100644 index 0000000..f15270b --- /dev/null +++ b/Generator/Rules/FG0010.md @@ -0,0 +1,18 @@ +# FG0010: Unused input interface + +## Cause +Method has an input parameter but the Funge source does not use any input instructions. + +## Solution +Remove the unused input parameter. + +## Example + +```cs +partial class Sample +{ + // Incorrect: input instruction not used + [GenerateFungeMethod(InlineSource = ">@")] + public static partial void Invalid(string input); +} +``` diff --git a/Generator/Rules/FG0011.md b/Generator/Rules/FG0011.md new file mode 100644 index 0000000..93e2c37 --- /dev/null +++ b/Generator/Rules/FG0011.md @@ -0,0 +1,18 @@ +# FG0011: Method must be partial + +## Cause +The method decorated with `GenerateFungeMethodAttribute` is not declared with the `partial` modifier. + +## Solution +Add the `partial` modifier to the method definition. + +## Example + +```cs +partial class Sample +{ + // Incorrect: not partial + [GenerateFungeMethod(InlineSource = ">@")] + public void Invalid(); +} +``` diff --git a/Generator/buildTransitive/Esolang.Funge.Generator.targets b/Generator/buildTransitive/Esolang.Funge.Generator.targets index 138fd6c..4ec9022 100644 --- a/Generator/buildTransitive/Esolang.Funge.Generator.targets +++ b/Generator/buildTransitive/Esolang.Funge.Generator.targets @@ -24,4 +24,10 @@ FungeLogicalPath="%(_FungeGeneratedFile.LogicalPath)" /> + + + + + + diff --git a/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj b/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj index 955ba42..df4e8b4 100644 --- a/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj +++ b/Interpreter.Tests/Esolang.Funge.Interpreter.Tests.csproj @@ -1,12 +1,7 @@ - net8.0;net9.0;net10.0 - enable - enable - false - true - false + net10.0 Funge.Interpreter.Tests diff --git a/Interpreter.Tests/ProgramTests.cs b/Interpreter.Tests/ProgramTests.cs index 22cf736..1e56786 100644 --- a/Interpreter.Tests/ProgramTests.cs +++ b/Interpreter.Tests/ProgramTests.cs @@ -1,29 +1,44 @@ -using Esolang.Funge.Interpreter; -using Microsoft.VisualStudio.TestTools.UnitTesting; - namespace Esolang.Funge.Interpreter.Tests; [TestClass] -public class ProgramTests +public class ProgramTests(TestContext TestContext) { - const string HelloWorldProgram = "64+\"!dlroW ,olleH\">:#,_@"; +#pragma warning disable MSTEST0054 + CancellationToken CancellationToken => TestContext.CancellationTokenSource.Token; +#pragma warning restore MSTEST0054 + + static int Run(string[] args) + { + var entryPoint = typeof(Program).Assembly.EntryPoint; + Assert.IsNotNull(entryPoint); + object?[] parameters = [args]; + var result = entryPoint.Invoke(null, parameters) as int?; + Assert.IsNotNull(result); + return result.Value; + } + + [TestMethod] + public void Run_Default_ReturnsOne() + { + var exitCode = Run([]); + Assert.AreEqual(1, exitCode); + } [TestMethod] - public async Task RunAsync_HelpOption_ReturnsZero() + public void Run_HelpOption_ReturnsZero() { - var exitCode = await Program.RunAsync(["--help"]); + var exitCode = Run(["--help"]); Assert.AreEqual(0, exitCode); } [TestMethod] - public async Task RunAsync_HelloWorld_ReturnsZero() + public async Task Run_HelloWorld_ReturnsZero() { var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.b98"); try { - await File.WriteAllTextAsync(path, HelloWorldProgram); - - var exitCode = await Program.RunAsync([path]); + await File.WriteAllTextAsync(path, "64+\"!dlroW ,olleH\">:#,_@", CancellationToken); + var exitCode = Run(["--path", path]); Assert.AreEqual(0, exitCode); } finally @@ -33,18 +48,43 @@ public async Task RunAsync_HelloWorld_ReturnsZero() } } + [TestMethod] + public void Run_SourceOptionWithMultilineCode_ReturnsZero() + { + const string source = "v\n>25*\"!dlroW ,olleH\",,,,@"; + var exitCode = Run(["--source", source]); + Assert.AreEqual(0, exitCode); + } + + [TestMethod] + public async Task Run_PathAndSourceTogether_ReturnsOne() + { + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.b98"); + try + { + await File.WriteAllTextAsync(path, "@", CancellationToken); + var exitCode = Run(["--path", path, "--source", "@"]); + Assert.AreEqual(1, exitCode); + } + finally + { + if (File.Exists(path)) + File.Delete(path); + } + } + [TestMethod] public async Task RunAsync_CancelledToken_StopsInfiniteProgram() { var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.b98"); try { - await File.WriteAllTextAsync(path, ">"); + await File.WriteAllTextAsync(path, ">", CancellationToken); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); - var exitCode = await Program.RunAsync([path], cancellation.Token); + var exitCode = await Program.RunAsync(["--path", path], cancellationToken: cancellation.Token); Assert.AreEqual(0, exitCode); } finally diff --git a/Interpreter/Esolang.Funge.Interpreter.csproj b/Interpreter/Esolang.Funge.Interpreter.csproj index 8db253a..38c4057 100644 --- a/Interpreter/Esolang.Funge.Interpreter.csproj +++ b/Interpreter/Esolang.Funge.Interpreter.csproj @@ -1,7 +1,7 @@ Exe - net8.0;net9.0;net10.0 + net10.0 dotnet-funge Funge-98 console interpreter. true @@ -13,13 +13,11 @@ Command-line interpreter for Funge-98 (Befunge-98) programs. dotnet-funge esolang;funge;funge-98;befunge;interpreter;cli;dotnet-tool - - - - true - true - true - true + true + false + false + true + win-x64;linux-x64;osx-arm64;any @@ -29,6 +27,7 @@ + diff --git a/Interpreter/FungeInterpreterExtensions.cs b/Interpreter/FungeInterpreterExtensions.cs index 830eea3..38225c5 100644 --- a/Interpreter/FungeInterpreterExtensions.cs +++ b/Interpreter/FungeInterpreterExtensions.cs @@ -1,5 +1,6 @@ using Esolang.Funge.Parser; using Esolang.Funge.Processor; +using Esolang.Interpreter; using System.Collections; using System.CommandLine; @@ -15,30 +16,48 @@ public static class FungeInterpreterExtensions /// public static RootCommand BuildRootCommand() { - var pathArgument = new Argument("path") + var pathOption = new Option(name: "--path", aliases: ["-p"]) { Description = "Path to a Funge-98 source file (.b98).", }; + var sourceOption = new Option(name: "--source", aliases: ["-s"]) + { + Description = "Inline Funge-98 source code. Newlines are supported.", + }; + var rootCommand = new RootCommand("Run Funge-98 (Befunge-98) programs.") { - pathArgument, + pathOption, + sourceOption, }; - rootCommand.SetAction((parseResult, cancellationToken) => + rootCommand.SetAction(async (parseResult, cancellationToken) => { - var path = parseResult.GetValue(pathArgument)!; - var space = FungeParser.ParseFile(path); + var path = parseResult.GetValue(pathOption); + var source = parseResult.GetValue(sourceOption); + + var hasPath = !string.IsNullOrWhiteSpace(path); + var hasSource = source is not null; + if (hasPath == hasSource) + { + Console.Error.WriteLine("Specify exactly one of --path/-p or --source/-s."); + return 1; + } + + var space = hasPath + ? FungeParser.ParseFile(path!) + : FungeParser.Parse(source!); var env = Environment.GetEnvironmentVariables() .Cast() .Select(static entry => $"{entry.Key}={entry.Value}"); + var inputArgument = hasPath ? path! : ""; var proc = new FungeProcessor( space, - Console.Out, - Console.In, - commandLineArguments: [path], + commandLineArguments: [inputArgument], environmentVariables: env); - return Task.FromResult(proc.RunToEnd(cancellationToken: cancellationToken)); + + return await proc.RunToConsoleAsync(cancellationToken); }); return rootCommand; diff --git a/Interpreter/Program.cs b/Interpreter/Program.cs index 4c0934d..c7c37cd 100644 --- a/Interpreter/Program.cs +++ b/Interpreter/Program.cs @@ -1,40 +1,30 @@ -namespace Esolang.Funge.Interpreter; +using Esolang.Funge.Interpreter; + +using var cancellation = new CancellationTokenSource(); +void OnCancelKeyPress(object? _, ConsoleCancelEventArgs e) +{ + e.Cancel = true; + cancellation.Cancel(); +} + +Console.CancelKeyPress += OnCancelKeyPress; +try +{ + return await RunAsync(args, cancellation.Token); +} +finally +{ + Console.CancelKeyPress -= OnCancelKeyPress; +} /// /// Entry point for the dotnet-funge command-line tool. /// -public static class Program +partial class Program { - /// - /// Runs the command-line pipeline and returns the process exit code. - /// - /// Command-line arguments. - /// Token to cancel command execution. - /// The exit code. public static async Task RunAsync(string[] args, CancellationToken cancellationToken = default) { var rootCommand = FungeInterpreterExtensions.BuildRootCommand(); 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/Interpreter/README.md b/Interpreter/README.md index dcde688..426816e 100644 --- a/Interpreter/README.md +++ b/Interpreter/README.md @@ -11,17 +11,30 @@ dotnet tool install -g dotnet-funge ## Usage ```bash -dotnet-funge +dotnet-funge --path ``` -| Argument | Description | +| Option | Description | | --- | --- | -| `` | Path to a Funge-98 source file (`.b98`) | +| `--path`, `-p` | Path to a Funge-98 source file (`.b98`) | +| `--source`, `-s` | Inline Funge-98 source code (supports newlines) | + +Specify exactly one of `--path` or `--source`. ### Example ```bash -dotnet-funge hello.b98 +dotnet-funge --path hello.b98 +``` + +### Inline Source Example (PowerShell) + +```powershell +$source = @' +v +>25*"!dlroW ,olleH",,,,@ +'@ +dotnet-funge --source $source ``` Standard input / output are connected to the running program (`~` / `&` for input, `,` / `.` for output). diff --git a/Parser.Tests/FungeParserTests.cs b/Parser.Tests/FungeParserTests.cs index 5e27f98..bfb5151 100644 --- a/Parser.Tests/FungeParserTests.cs +++ b/Parser.Tests/FungeParserTests.cs @@ -1,5 +1,3 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - namespace Esolang.Funge.Parser.Tests; [TestClass] diff --git a/Parser/Esolang.Funge.Parser.csproj b/Parser/Esolang.Funge.Parser.csproj index 3ee9197..5e7b824 100644 --- a/Parser/Esolang.Funge.Parser.csproj +++ b/Parser/Esolang.Funge.Parser.csproj @@ -9,13 +9,6 @@ esolang;funge;funge-98;befunge;parser - - true - true - true - true - - true false diff --git a/Parser/FungeSpace.cs b/Parser/FungeSpace.cs index 11024a3..826527a 100644 --- a/Parser/FungeSpace.cs +++ b/Parser/FungeSpace.cs @@ -6,11 +6,11 @@ namespace Esolang.Funge.Parser; /// public sealed class FungeSpace { - private readonly Dictionary _cells = new(); - private int _minX, _minY, _minZ, _maxX, _maxY, _maxZ; - private bool _hasAny; + readonly Dictionary _cells = []; + int _minX, _minY, _minZ, _maxX, _maxY, _maxZ; + bool _hasAny; - private void IncludeInBounds(FungeVector pos) + void IncludeInBounds(FungeVector pos) { if (!_hasAny) { @@ -96,19 +96,19 @@ public FungeVector Advance(FungeVector pos, FungeVector delta) var depth = _maxZ - _minZ + 1; if (nextX < _minX) - nextX = _maxX - ((_minX - nextX - 1) % width); + nextX = _maxX - (_minX - nextX - 1) % width; else if (nextX > _maxX) - nextX = _minX + ((nextX - _maxX - 1) % width); + nextX = _minX + (nextX - _maxX - 1) % width; if (nextY < _minY) - nextY = _maxY - ((_minY - nextY - 1) % height); + nextY = _maxY - (_minY - nextY - 1) % height; else if (nextY > _maxY) - nextY = _minY + ((nextY - _maxY - 1) % height); + nextY = _minY + (nextY - _maxY - 1) % height; if (nextZ < _minZ) - nextZ = _maxZ - ((_minZ - nextZ - 1) % depth); + nextZ = _maxZ - (_minZ - nextZ - 1) % depth; else if (nextZ > _maxZ) - nextZ = _minZ + ((nextZ - _maxZ - 1) % depth); + nextZ = _minZ + (nextZ - _maxZ - 1) % depth; return new FungeVector(nextX, nextY, nextZ); } diff --git a/Parser/Shared/HashCode.cs b/Parser/Shared/HashCode.cs index 96fdf12..54f2cb5 100644 --- a/Parser/Shared/HashCode.cs +++ b/Parser/Shared/HashCode.cs @@ -2,7 +2,7 @@ // Minimal HashCode polyfill for netstandard2.0 namespace System; -internal static class HashCode +static class HashCode { public static int Combine(T1 v1, T2 v2) { diff --git a/Parser/Shared/IsExternalInit.cs b/Parser/Shared/IsExternalInit.cs index 1330bba..2573b2d 100644 --- a/Parser/Shared/IsExternalInit.cs +++ b/Parser/Shared/IsExternalInit.cs @@ -4,5 +4,5 @@ namespace System.Runtime.CompilerServices; -internal sealed class IsExternalInit { } +sealed class IsExternalInit { } #endif diff --git a/Processor.Tests/FungeProcessorTests.cs b/Processor.Tests/FungeProcessorTests.cs index b8337a5..c57c159 100644 --- a/Processor.Tests/FungeProcessorTests.cs +++ b/Processor.Tests/FungeProcessorTests.cs @@ -1,31 +1,60 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using static Esolang.Processor.IOEvent; namespace Esolang.Funge.Processor.Tests; [TestClass] public class FungeProcessorTests(TestContext TestContext) { - CancellationToken TestCancellationToken => TestContext.CancellationTokenSource.Token; - private string Run(string source, string? input = null) + CancellationToken TestCancellationToken => TestContext.CancellationToken; + string Run(string source, string? input = null) { var space = Parser.FungeParser.Parse(source); var output = new StringWriter(); var reader = input is null ? TextReader.Null : new StringReader(input); - var proc = new FungeProcessor(space, output, reader); - proc.Run(TestCancellationToken); + var proc = new FungeProcessor(space); + _ = RunToEnd(proc, reader, output, TestCancellationToken); return output.ToString(); } - private int RunGetExitCode(string source) + static int RunToEnd(FungeProcessor proc, TextReader input, TextWriter output, CancellationToken ct) + { + var task = Task.Run(async () => + { + var exitCode = 0; + await foreach (var ev in proc.RunAsyncEnumerable(ct)) + { + switch (ev) + { + case OutputCharEvent oce: output.Write(oce.Output); break; + case OutputIntEvent oie: output.Write(oie.Output); break; + case InputCharEvent ice: + var c = input.Read(); + if (c != -1) ice.Write((char)c); + break; + case InputIntEvent iie: + var line = input.ReadLine(); + if (int.TryParse(line, out var val)) iie.Write(val); + break; + case EndEvent ee: + exitCode = ee.ExitCode; + break; + } + } + return exitCode; + }, ct); + return task.GetAwaiter().GetResult(); + } + + int RunGetExitCode(string source) { var space = Parser.FungeParser.Parse(source); - var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); - return proc.Run(TestCancellationToken); + var proc = new FungeProcessor(space); + return RunToEnd(proc, TextReader.Null, TextWriter.Null, TestCancellationToken); } [TestMethod] [Timeout(Constant.Timeout, CooperativeCancellation = true)] - public async Task TestDirectionalInstructions() + public void TestDirectionalInstructions() { var space = new Parser.FungeSpace(); var pos1 = new Parser.FungeVector(0, 0, 0); @@ -40,14 +69,14 @@ public async Task TestDirectionalInstructions() space[pos4] = 'v'; space[pos5] = '@'; - var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); + var proc = new FungeProcessor(space); var token = TestCancellationToken; - await proc.RunToEndAsync(null, null, token); + RunToEnd(proc, TextReader.Null, TextWriter.Null, token); } - private static string EncodeZeroGnirts(string value) - => $"0\"{new string(value.Reverse().ToArray())}\""; + static string EncodeZeroGnirts(string value) + => $"0\"{new string([.. value.Reverse()])}\""; // ── Termination ──────────────────────────────────────────────────────── @@ -375,21 +404,21 @@ public void RunToEnd_UsesProvidedTextIo() var space = Parser.FungeParser.Parse("&.@"); var output = new StringWriter(); var input = new StringReader("42\n"); - var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); + var proc = new FungeProcessor(space); - var exitCode = proc.RunToEnd(input, output, TestCancellationToken); + var exitCode = RunToEnd(proc, input, output, TestCancellationToken); Assert.AreEqual(0, exitCode); Assert.AreEqual("42 ", output.ToString()); } [TestMethod] - public async Task RunToEndAsync_ReturnsExitCode() + public void RunToEndAsync_ReturnsExitCode() { var space = Parser.FungeParser.Parse("7q"); - var proc = new FungeProcessor(space, TextWriter.Null, TextReader.Null); + var proc = new FungeProcessor(space); - var exitCode = await proc.RunToEndAsync(cancellationToken: TestCancellationToken); + var exitCode = RunToEnd(proc, TextReader.Null, TextWriter.Null, TestCancellationToken); Assert.AreEqual(7, exitCode); } diff --git a/Processor.Tests/InstructionPointerTests.cs b/Processor.Tests/InstructionPointerTests.cs index a5f3ab8..e92013b 100644 --- a/Processor.Tests/InstructionPointerTests.cs +++ b/Processor.Tests/InstructionPointerTests.cs @@ -1,34 +1,33 @@ -using Esolang.Funge.Parser; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Esolang.Funge.Processor.Tests; - -[TestClass] -public class InstructionPointerTests -{ - [TestMethod] - public void CreateChild_CopiesStateCorrectly() - { - var parent = new InstructionPointer(1) - { - Position = new FungeVector(1, 2, 3), - Delta = new FungeVector(0, 1, 0), - Offset = new FungeVector(10, 10, 10), - StringMode = true - }; - parent.StackStack.Push(42); - - var child = parent.CreateChild(2); - - Assert.AreEqual(2, child.Id); - Assert.AreEqual(parent.Position, child.Position); - Assert.AreNotEqual(parent.Delta, child.Delta); // Should be reflected - Assert.AreEqual(parent.Offset, child.Offset); - Assert.AreEqual(parent.StringMode, child.StringMode); - - // Stack should be cloned - Assert.AreEqual(42, child.StackStack.Pop()); - child.StackStack.Push(99); - Assert.AreEqual(42, parent.StackStack.Pop()); // Original unaffected - } -} +using Esolang.Funge.Parser; + +namespace Esolang.Funge.Processor.Tests; + +[TestClass] +public class InstructionPointerTests +{ + [TestMethod] + public void CreateChild_CopiesStateCorrectly() + { + var parent = new InstructionPointer(1) + { + Position = new FungeVector(1, 2, 3), + Delta = new FungeVector(0, 1, 0), + Offset = new FungeVector(10, 10, 10), + StringMode = true + }; + parent.StackStack.Push(42); + + var child = parent.CreateChild(2); + + Assert.AreEqual(2, child.Id); + Assert.AreEqual(parent.Position, child.Position); + Assert.AreNotEqual(parent.Delta, child.Delta); // Should be reflected + Assert.AreEqual(parent.Offset, child.Offset); + Assert.AreEqual(parent.StringMode, child.StringMode); + + // Stack should be cloned + Assert.AreEqual(42, child.StackStack.Pop()); + child.StackStack.Push(99); + Assert.AreEqual(42, parent.StackStack.Pop()); // Original unaffected + } +} diff --git a/Processor.Tests/StackStackTests.cs b/Processor.Tests/StackStackTests.cs index 010c976..7ab6841 100644 --- a/Processor.Tests/StackStackTests.cs +++ b/Processor.Tests/StackStackTests.cs @@ -1,51 +1,49 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Esolang.Funge.Processor.Tests; - -[TestClass] -public class StackStackTests -{ - [TestMethod] - public void PushPop_MaintainsLIFO() - { - var ss = new StackStack(); - ss.Push(1); - ss.Push(2); - Assert.AreEqual(2, ss.Pop()); - Assert.AreEqual(1, ss.Pop()); - Assert.AreEqual(0, ss.Pop()); // Empty returns 0 - } - - [TestMethod] - public void StackStackOperations_ManageStacksCorrectly() - { - var ss = new StackStack(); - ss.Push(1); - ss.PushNewStack(); - ss.Push(2); - - Assert.AreEqual(2, ss.TOSS.Peek()); - Assert.IsTrue(ss.HasSOSS); - Assert.AreEqual(2, ss.StackCount); - - ss.PopCurrentStack(); - Assert.AreEqual(1, ss.TOSS.Peek()); - Assert.IsFalse(ss.HasSOSS); - Assert.AreEqual(1, ss.StackCount); - } - - [TestMethod] - public void Clone_CreatesDeepCopy() - { - var ss = new StackStack(); - ss.Push(1); - ss.PushNewStack(); - ss.Push(2); - - var clone = ss.Clone(); - Assert.AreEqual(2, clone.Pop()); - - // Ensure original is unaffected - Assert.AreEqual(2, ss.TOSS.Peek()); - } -} +namespace Esolang.Funge.Processor.Tests; + +[TestClass] +public class StackStackTests +{ + [TestMethod] + public void PushPop_MaintainsLIFO() + { + var ss = new StackStack(); + ss.Push(1); + ss.Push(2); + Assert.AreEqual(2, ss.Pop()); + Assert.AreEqual(1, ss.Pop()); + Assert.AreEqual(0, ss.Pop()); // Empty returns 0 + } + + [TestMethod] + public void StackStackOperations_ManageStacksCorrectly() + { + var ss = new StackStack(); + ss.Push(1); + ss.PushNewStack(); + ss.Push(2); + + Assert.AreEqual(2, ss.TOSS.Peek()); + Assert.IsTrue(ss.HasSOSS); + Assert.AreEqual(2, ss.StackCount); + + ss.PopCurrentStack(); + Assert.AreEqual(1, ss.TOSS.Peek()); + Assert.IsFalse(ss.HasSOSS); + Assert.AreEqual(1, ss.StackCount); + } + + [TestMethod] + public void Clone_CreatesDeepCopy() + { + var ss = new StackStack(); + ss.Push(1); + ss.PushNewStack(); + ss.Push(2); + + var clone = ss.Clone(); + Assert.AreEqual(2, clone.Pop()); + + // Ensure original is unaffected + Assert.AreEqual(2, ss.TOSS.Peek()); + } +} diff --git a/Processor/Esolang.Funge.Processor.csproj b/Processor/Esolang.Funge.Processor.csproj index caca1ea..57a389d 100644 --- a/Processor/Esolang.Funge.Processor.csproj +++ b/Processor/Esolang.Funge.Processor.csproj @@ -9,20 +9,12 @@ esolang;funge;funge-98;befunge;processor - - true - true - true - true - - true false - @@ -30,6 +22,10 @@ + + + + diff --git a/Processor/FungeProcessor.IEventProcessor.cs b/Processor/FungeProcessor.IEventProcessor.cs new file mode 100644 index 0000000..1184fa4 --- /dev/null +++ b/Processor/FungeProcessor.IEventProcessor.cs @@ -0,0 +1,58 @@ +using Esolang.Processor; +using System.Runtime.CompilerServices; +using static Esolang.Processor.IOEvent; + +namespace Esolang.Funge.Processor; + +public sealed partial class FungeProcessor : IEventProcessor +{ + + sealed class FungeState + { + public int ExitCode; + public bool Quit; + public bool SuppressAdvance; + } + + /// + /// Runs the Funge-98 program and returns the process exit code. + /// The program starts with a single IP at (0,0) moving East. + /// + /// Token to cancel execution. + /// Exit code: 0 unless the program used q. + public async IAsyncEnumerable RunAsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var ips = new LinkedList(); + ips.AddFirst(new InstructionPointer(_nextIpId++)); + var state = new FungeState(); + + while (ips.Count > 0 && !state.Quit && !cancellationToken.IsCancellationRequested) + { + var node = ips.First; + while (node is not null && !state.Quit && !cancellationToken.IsCancellationRequested) + { + var nextNode = node.Next; + var ip = node.Value; + + state.SuppressAdvance = false; + foreach (var ev in ExecuteInstruction(ip, ips, node, state)) + { + yield return ev; + } + + if (ip.IsStopped || state.Quit) + { + ips.Remove(node); + } + else if (!state.SuppressAdvance) + { + ip.Position = _space.Advance(ip.Position, ip.Delta); + } + + node = nextNode; + } + } + + yield return new EndEvent(state.ExitCode); + } +} diff --git a/Processor/FungeProcessor.IProcessor.cs b/Processor/FungeProcessor.IProcessor.cs new file mode 100644 index 0000000..50d3b2b --- /dev/null +++ b/Processor/FungeProcessor.IProcessor.cs @@ -0,0 +1,13 @@ + +using Esolang.Funge.Parser; +using Esolang.Processor; +using System.Diagnostics.CodeAnalysis; + +namespace Esolang.Funge.Processor; + +public partial class FungeProcessor : IProcessor +{ + /// + [ExcludeFromCodeCoverage] + FungeSpace IProcessor.Program => _space; +} diff --git a/Processor/FungeProcessor.cs b/Processor/FungeProcessor.cs index 3b5d8a7..7d70501 100644 --- a/Processor/FungeProcessor.cs +++ b/Processor/FungeProcessor.cs @@ -2,6 +2,7 @@ using Esolang.Processor; using System.Collections; using System.Diagnostics; +using static Esolang.Processor.IOEvent; namespace Esolang.Funge.Processor; @@ -16,94 +17,30 @@ namespace Esolang.Funge.Processor; /// Initializes a new with the given program space and optional I/O. /// /// The parsed Funge-98 program space. -/// Output writer; defaults to . -/// Input reader; defaults to . /// Optional command-line arguments exposed by y. Defaults to host process args. /// Optional environment variable entries (NAME=VALUE) exposed by y. Defaults to host process environment. public sealed partial class FungeProcessor( FungeSpace space, - TextWriter? output = null, - TextReader? input = null, IEnumerable? commandLineArguments = null, - IEnumerable? environmentVariables = null) : ITextProcessor + IEnumerable? environmentVariables = null) { - private readonly FungeSpace _space = space; - private readonly TextWriter _output = output ?? Console.Out; - private readonly TextReader _input = input ?? Console.In; - private readonly string[] _commandLineArguments = (commandLineArguments ?? Environment.GetCommandLineArgs()) + readonly FungeSpace _space = space; + readonly string[] _commandLineArguments = (commandLineArguments ?? Environment.GetCommandLineArgs()) #pragma warning disable IDE0305 // コレクションの初期化を簡略化します .ToArray(); #pragma warning restore IDE0305 // コレクションの初期化を簡略化します - private readonly string[] _environmentVariables = [.. environmentVariables + readonly string[] _environmentVariables = [.. environmentVariables ?? Environment.GetEnvironmentVariables() .Cast() .Select(static entry => $"{entry.Key}={entry.Value}")]; - private readonly Random _random = new(); - private int _nextIpId; + readonly Random _random = new(); + int _nextIpId; - /// - public FungeSpace Program => _space; - - /// - /// Runs the Funge-98 program and returns the process exit code. - /// The program starts with a single IP at (0,0) moving East. - /// - /// Token to cancel execution. - /// Exit code: 0 unless the program used q. - public int Run(CancellationToken cancellationToken = default) - => RunToEnd(null, null, cancellationToken); - - /// - public int RunToEnd(TextReader? input = null, TextWriter? output = null, CancellationToken cancellationToken = default) - { - 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( + IEnumerable ExecuteInstruction( InstructionPointer ip, LinkedList ips, LinkedListNode ipNode, - ref int exitCode, - ref bool quit, - ref bool suppressAdvance, - TextReader input, - TextWriter output, + FungeState state, int? overrideCell = null) { var cell = overrideCell ?? _space[ip.Position]; @@ -138,7 +75,7 @@ private void ExecuteInstruction( { ip.StackStack.Push(cell); } - return; + yield break; } switch (cell) @@ -313,7 +250,7 @@ private void ExecuteInstruction( var dir = s >= 0 ? ip.Delta : ip.Delta.Reflect(); for (var i = 0; i < Math.Abs(s); i++) ip.Position = _space.Advance(ip.Position, dir); - suppressAdvance = true; + state.SuppressAdvance = true; break; } @@ -360,27 +297,31 @@ private void ExecuteInstruction( // ── I/O ────────────────────────────────────────────────────────── case '.': // Output Integer - output.Write(ip.StackStack.Pop()); - output.Write(' '); + yield return OutputInt(ip.StackStack.Pop()); + yield return OutputChar(' '); break; case ',': // Output Character - output.Write((char)ip.StackStack.Pop()); + yield return OutputChar((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); + int? input = null; + var ev = InputInt(value => input = value); + yield return ev; + if (input.HasValue) ip.StackStack.Push(input.Value); + else ip.Delta = ip.Delta.Reflect(); break; } case '~': // Input Character { - var ch = input.Read(); - if (ch < 0) ip.Delta = ip.Delta.Reflect(); - else ip.StackStack.Push(ch); + char? input = null; + var ev = InputChar(value => input = value); + yield return ev; + if (input.HasValue) ip.StackStack.Push(input.Value); + else ip.Delta = ip.Delta.Reflect(); break; } @@ -431,8 +372,8 @@ private void ExecuteInstruction( break; case 'q': // Quit program immediately - exitCode = ip.StackStack.Pop(); - quit = true; + state.ExitCode = ip.StackStack.Pop(); + state.Quit = true; break; case 'k': // Iterate: execute next instruction n times @@ -467,7 +408,7 @@ private void ExecuteInstruction( { // n=0: skip the operand. IP moves to the position AFTER the operand. ip.Position = _space.Advance(instrPos, ip.Delta); - suppressAdvance = true; + state.SuppressAdvance = true; } else { @@ -476,10 +417,13 @@ private void ExecuteInstruction( // After k finishes, normal advancement continues from the IP's current position, // so position-changing operands such as [ and # behave "from k". var operand = _space[instrPos]; - for (var i = 0; i < n && !ip.IsStopped && !quit; i++) + for (var i = 0; i < n && !ip.IsStopped && !state.Quit; i++) { - var dummy = false; - ExecuteInstruction(ip, ips, ipNode, ref exitCode, ref quit, ref dummy, input, output, operand); + var subState = new FungeState { ExitCode = state.ExitCode, Quit = state.Quit, SuppressAdvance = false }; + foreach (var ev in ExecuteInstruction(ip, ips, ipNode, subState, operand)) + yield return ev; + state.ExitCode = subState.ExitCode; + state.Quit = subState.Quit; } } break; @@ -626,7 +570,7 @@ private void ExecuteInstruction( } } - private static FungeVector PopVector(StackStack stack) + static FungeVector PopVector(StackStack stack) { var z = stack.Pop(); var y = stack.Pop(); @@ -634,14 +578,14 @@ private static FungeVector PopVector(StackStack stack) return new FungeVector(x, y, z); } - private static void PushVector(StackStack stack, FungeVector vector) + static void PushVector(StackStack stack, FungeVector vector) { stack.Push(vector.X); stack.Push(vector.Y); stack.Push(vector.Z); } - private static bool TryPopZeroTerminatedString(StackStack stack, out string result) + static bool TryPopZeroTerminatedString(StackStack stack, out string result) { var chars = new List(); while (true) @@ -663,7 +607,7 @@ private static bool TryPopZeroTerminatedString(StackStack stack, out string resu } } - private bool TryInputFile(FungeVector leastPoint, string fileName, bool binaryMode, out FungeVector size) + bool TryInputFile(FungeVector leastPoint, string fileName, bool binaryMode, out FungeVector size) { size = new FungeVector(0, 0, 0); @@ -727,7 +671,7 @@ private bool TryInputFile(FungeVector leastPoint, string fileName, bool binaryMo return true; } - private bool TryOutputFile(FungeVector leastPoint, FungeVector size, string fileName, bool linearText) + bool TryOutputFile(FungeVector leastPoint, FungeVector size, string fileName, bool linearText) { var sx = Math.Max(0, size.X); var sy = Math.Max(0, size.Y); @@ -773,7 +717,7 @@ private bool TryOutputFile(FungeVector leastPoint, FungeVector size, string file } } - private static int ExecuteSystemCommand(string command) + static int ExecuteSystemCommand(string command) { try { @@ -816,7 +760,7 @@ private static int ExecuteSystemCommand(string command) /// If is greater than zero, only item /// (1-indexed from top) is left on the stack. /// - private void PushSysInfo(InstructionPointer ip, int _, int c) + void PushSysInfo(InstructionPointer ip, int _, int c) { // Build list of items in order: items[0] will be last-pushed (item 1 from top) List items = []; @@ -862,9 +806,9 @@ private void PushSysInfo(InstructionPointer ip, int _, int c) var now = DateTime.Now; // 20. Current date: (year-1900)*256*256 + month*256 + day - items.Add(((now.Year - 1900) * 256 * 256) + (now.Month * 256) + now.Day); + items.Add((now.Year - 1900) * 256 * 256 + now.Month * 256 + now.Day); // 21. Current time: HH*256*256 + MM*256 + SS - items.Add((now.Hour * 256 * 256) + (now.Minute * 256) + now.Second); + items.Add(now.Hour * 256 * 256 + now.Minute * 256 + now.Second); // 22. Number of stacks in stack stack items.Add(ip.StackStack.StackCount); // 23+. Size of each stack (TOSS first) diff --git a/Processor/InstructionPointer.cs b/Processor/InstructionPointer.cs index 3dec861..5cf19bc 100644 --- a/Processor/InstructionPointer.cs +++ b/Processor/InstructionPointer.cs @@ -31,7 +31,7 @@ public sealed class InstructionPointer /// Initializes an IP with the given ID and a new empty stack stack. public InstructionPointer(int id) : this(id, new StackStack()) { } - private InstructionPointer(int id, StackStack stackStack) + InstructionPointer(int id, StackStack stackStack) { Id = id; StackStack = stackStack; diff --git a/Processor/README.md b/Processor/README.md index 3addb62..b6781c9 100644 --- a/Processor/README.md +++ b/Processor/README.md @@ -65,12 +65,14 @@ using Esolang.Funge.Parser; using Esolang.Funge.Processor; var space = FungeParser.ParseFile("hello.b98"); -var proc = new FungeProcessor(space, Console.Out, Console.In); -int exitCode = proc.Run(); +var proc = new FungeProcessor(space); +int exitCode = await proc.RunToEndAsync(); ``` -`FungeProcessor` accepts optional `TextWriter` (output) and `TextReader` (input) arguments, defaulting to `Console.Out` / `Console.In`. -`Run()` accepts an optional `CancellationToken` and returns the exit code set by `q` (0 if not used). +`FungeProcessor` executes programs via an event stream. You can run it to completion using `RunToEndAsync()` (or the synchronous `Run()`), which defaults to `Console.In` / `Console.Out`. +For fine-grained control, use `RunAsyncEnumerable()` to handle I/O events manually. + +`Run()` and `RunToEndAsync()` accept optional `TextReader` and `TextWriter` arguments, and an optional `CancellationToken`. They return the exit code set by `q` (0 if not used). ## References diff --git a/Processor/StackStack.cs b/Processor/StackStack.cs index d101cde..884fc43 100644 --- a/Processor/StackStack.cs +++ b/Processor/StackStack.cs @@ -7,12 +7,12 @@ namespace Esolang.Funge.Processor; /// public sealed class StackStack { - private readonly LinkedList> _stacks = new(); + readonly LinkedList> _stacks = new(); /// Initializes a new stack stack with a single empty TOSS. public StackStack() => _stacks.AddFirst(new Stack()); - private StackStack(LinkedList> stacks) => _stacks = stacks; + StackStack(LinkedList> stacks) => _stacks = stacks; /// Gets the top-of-stack-stack (current active stack). public Stack TOSS => _stacks.First!.Value; diff --git a/README.md b/README.md index adc7184..a052f13 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,10 @@ dotnet tool install -g dotnet-funge | Project | NuGet | Summary | |---|---|---| -| [dotnet-funge](./Interpreter/README.md) | [![NuGet: dotnet-funge](https://img.shields.io/nuget/v/dotnet-funge?logo=nuget&label=1.1.1)](https://www.nuget.org/packages/dotnet-funge/) | Funge-98 command-line interpreter. | -| [Esolang.Funge.Generator](./Generator/README.md) | [![NuGet: Esolang.Funge.Generator](https://img.shields.io/nuget/v/Esolang.Funge.Generator?logo=nuget&label=1.1.1)](https://www.nuget.org/packages/Esolang.Funge.Generator/) | Funge-98 source generator. | -| [Esolang.Funge.Parser](./Parser/README.md) | [![NuGet: Esolang.Funge.Parser](https://img.shields.io/nuget/v/Esolang.Funge.Parser?logo=nuget&label=1.1.1)](https://www.nuget.org/packages/Esolang.Funge.Parser/) | Funge-98 source parser. | -| [Esolang.Funge.Processor](./Processor/README.md) | [![NuGet: Esolang.Funge.Processor](https://img.shields.io/nuget/v/Esolang.Funge.Processor?logo=nuget&label=1.1.1)](https://www.nuget.org/packages/Esolang.Funge.Processor/) | Funge-98 execution engine. | +| [dotnet-funge](./Interpreter/README.md) | [![NuGet: dotnet-funge](https://img.shields.io/nuget/v/dotnet-funge?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/dotnet-funge/) | Funge-98 command-line interpreter. | +| [Esolang.Funge.Generator](./Generator/README.md) | [![NuGet: Esolang.Funge.Generator](https://img.shields.io/nuget/v/Esolang.Funge.Generator?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/Esolang.Funge.Generator/) | Funge-98 source generator. | +| [Esolang.Funge.Parser](./Parser/README.md) | [![NuGet: Esolang.Funge.Parser](https://img.shields.io/nuget/v/Esolang.Funge.Parser?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/Esolang.Funge.Parser/) | Funge-98 source parser. | +| [Esolang.Funge.Processor](./Processor/README.md) | [![NuGet: Esolang.Funge.Processor](https://img.shields.io/nuget/v/Esolang.Funge.Processor?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/Esolang.Funge.Processor/) | Funge-98 execution engine. | ## Framework Support diff --git a/coverlet.collect.runsettings b/coverlet.collect.runsettings deleted file mode 100644 index 3c39003..0000000 --- a/coverlet.collect.runsettings +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - cobertura - - - - - diff --git a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs index b6abc91..9f8c47a 100644 --- a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs +++ b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.cs @@ -13,7 +13,7 @@ Console.WriteLine($"{nameof(FungeSample.HelloWorldWriter)}: {textWriter}"); // IEnumerable (file-based) -Console.WriteLine($"{nameof(FungeSample.HelloWorldBytes)}: {Encoding.UTF8.GetString(FungeSample.HelloWorldBytes().ToArray())}"); +Console.WriteLine($"{nameof(FungeSample.HelloWorldBytes)}: {Encoding.UTF8.GetString([.. FungeSample.HelloWorldBytes()])}"); // IAsyncEnumerable (file-based) Console.WriteLine($"{nameof(FungeSample.HelloWorldBytesAsync)}: {Encoding.UTF8.GetString(await ToByteArrayAsync(FungeSample.HelloWorldBytesAsync()))}"); diff --git a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.csproj b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.csproj index 7af6468..8abd83a 100644 --- a/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.csproj +++ b/samples/Generator.UseConsole/Esolang.Funge.Generator.UseConsole.csproj @@ -15,6 +15,15 @@ + + + + + + +