diff --git a/.editorconfig b/.editorconfig index 7a8f4c4..2703074 100644 --- a/.editorconfig +++ b/.editorconfig @@ -39,7 +39,7 @@ dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning # Accessibility modifier settings -dotnet_style_require_accessibility_modifiers = for_non_interface_members +dotnet_style_require_accessibility_modifiers = omit_if_default # Expression-level preferences dotnet_style_coalesce_expression = true:warning @@ -236,7 +236,81 @@ dotnet_style_qualification_for_field = false:silent dotnet_style_qualification_for_property = false:warning dotnet_style_qualification_for_method = false:warning dotnet_style_qualification_for_event = false:warning -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning +# 関係演算子の優先順位が明確な場合は、かっこを使用しない +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:warning +# 関係演算子の優先順位が明確な場合は、かっこを使用しない +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:warning +# 関係演算子の優先順位が明確な場合は、かっこを使用しない +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:warning +# 算術演算子の優先順位が明確な場合はかっこを使用しない dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning +# IEnumerable list = new List() { 1, 2 };など、型が緩やかに一致する場合でもコレクション式を使用することを好みます。 対象となる型は、右側の型と一致するか、IEnumerable、ICollection、IList、IReadOnlyCollection、IReadOnlyListのいずれかの型である必要があります。 +dotnet_style_prefer_collection_expression = when_types_loosely_match:warning +# 不要な using ディレクティブを削除する (IDE0005) +dotnet_diagnostic.IDE0005.severity = warning +# is 式と型キャストの代わりにパターン マッチングを使用します。 +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +# 既定の修飾子である場合を除き、アクセシビリティ修飾子を優先します。 +dotnet_style_require_accessibility_modifiers = omit_if_default:warning +# 未使用のプライベート メンバーを削除する (IDE0051) +dotnet_diagnostic.IDE0051.severity = warning + +#region +# CA2012: ValueTask を正しく使用する必要があります +dotnet_diagnostic.CA2012.severity = warning +# CA2016: CancellationToken パラメーターを受け取るメソッドに渡す +dotnet_diagnostic.CA2016.severity = warning +# CA2200: スタックの詳細を保持するために再スローします。 +dotnet_diagnostic.CA2200.severity = warning +# CA2217: 列挙型に FlagsAttributeを付与しないでください。 +dotnet_diagnostic.CA2217.severity = warning +#endregion + +#region MSTest Analyzers + +# MSTEST0017: アサーション引数は正しい順序で渡す必要があります +dotnet_diagnostic.MSTEST0017.severity = warning +# MSTEST0020: TestInitialize メソッドよりもコンストラクターを優先する +dotnet_diagnostic.MSTEST0020.severity = warning +# MSTEST0021: TestCleanup メソッドよりも Dispose を優先する +dotnet_diagnostic.MSTEST0021.severity = warning +# MSTEST0022: Dispose メソッドよりも TestCleanup を優先する +dotnet_diagnostic.MSTEST0022.severity = warning +# MSTEST0023: ブール アサーションを否定しないこと +dotnet_diagnostic.MSTEST0023.severity = warning +# MSTEST0024: TestContext を静的メンバーに格納しない +dotnet_diagnostic.MSTEST0024.severity = warning +# MSTEST0025: 常に失敗するアサートではなく 'Assert.Fail' を使う +dotnet_diagnostic.MSTEST0025.severity = warning +# MSTEST0026: アサーションでの条件付きアクセスを回避する +dotnet_diagnostic.MSTEST0026.severity = warning +# MSTEST0029: パブリック メソッドをテスト メソッドにする必要がある +dotnet_diagnostic.MSTEST0029.severity = warning +# MSTEST0032: 条件が常に真であることがわかっているため、アサーションを確認するか削除してください +dotnet_diagnostic.MSTEST0032.severity = warning +# MSTEST0036: テスト クラス内でシャドウイングを使用しない +dotnet_diagnostic.MSTEST0036.severity = warning +# MSTEST0037: 適切な 'Assert' メソッドを使用する +dotnet_diagnostic.MSTEST0037.severity = warning +# MSTEST0038: 値型では 'Assert.AreSame' または 'Assert.AreNotSame' を使用しない +dotnet_diagnostic.MSTEST0038.severity = warning +# MSTEST0040: 'async void' コンテキスト内でアサートしない +dotnet_diagnostic.MSTEST0040.severity = warning +# MSTEST0044: DataTestMethod よりも TestMethod を優先する +dotnet_diagnostic.MSTEST0044.severity = warning +# MSTEST0045: タイムアウトに協調キャンセルを使用する +dotnet_diagnostic.MSTEST0045.severity = warning +# MSTEST0046: StringAssertではなくAssertを使います +dotnet_diagnostic.MSTEST0046.severity = warning +# MSTEST0049: Flow TestContext キャンセル トークンを使用する +dotnet_diagnostic.MSTEST0049.severity = warning +# MSTEST0051: Assert.Throws には 1 つのステートメントのみを含める必要があります +dotnet_diagnostic.MSTEST0051.severity = warning +# MSTEST0054: TestContext.CancellationToken からのキャンセル トークンを使用する +dotnet_diagnostic.MSTEST0054.severity = warning +# MSTEST0058: catch ブロック内の assert を避ける +dotnet_diagnostic.MSTEST0058.severity = warning +# MSTEST0061: ランタイム チェックの代わりに OSCondition 属性を使用する +dotnet_diagnostic.MSTEST0061.severity = warning + +#endregion \ No newline at end of file diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 9c4f64d..bfeb2e1 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,4 +1,4 @@ -name: build +name: build and test on: push: @@ -12,35 +12,38 @@ on: env: DOTNET_VERSION: '10.0.x' - NUGET_SOURCE: 'https://api.nuget.org/v3/index.json' jobs: - build: - name: build-${{matrix.os}} + build-and-test: + + name: build-and-test-${{matrix.os}} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup .NET Core - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Install dependencies - run: dotnet restore --source "${{ env.NUGET_SOURCE }}" - + run: dotnet restore + - name: Build run: dotnet build --no-restore + + - name: Test + run: dotnet test --no-restore --verbosity normal - name: Pack if: ${{ matrix.os == 'ubuntu-latest' }} run: | dotnet pack -o artifacts/ - - - uses: actions/upload-artifact@v4 + + - uses: actions/upload-artifact@v7 if: ${{ matrix.os == 'ubuntu-latest' }} with: name: artifacts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 45de965..a725457 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,28 +17,32 @@ permissions: env: DOTNET_VERSION: '10.0.x' - NUGET_SOURCE: 'https://api.nuget.org/v3/index.json' jobs: publish: name: publish-packages-and-release 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: 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 run: | + dotnet pack Generator.Abstractions/Esolang.Generator.Abstractions.csproj -c Release -o artifacts/nuget + dotnet pack Interpreter.Abstractions/Esolang.Interpreter.Abstractions.csproj -c Release -o artifacts/nuget dotnet pack Processor.Abstractions/Esolang.Processor.Abstractions.csproj -c Release -o artifacts/nuget + dotnet pack Processor.Extensions.IO/Esolang.Processor.Extensions.IO.csproj -c Release -o artifacts/nuget - name: Publish to NuGet.org env: NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} @@ -93,7 +97,19 @@ jobs: set -euo pipefail tag="${{ github.event.inputs.tag || github.ref_name }}" shopt -s nullglob - gh release create "$tag" \ - --repo "${{ github.repository }}" \ - --title "$tag" \ - artifacts/nuget/*.nupkg artifacts/nuget/*.snupkg || true + assets=(artifacts/nuget/*.nupkg artifacts/nuget/*.snupkg) + 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 + else + gh release create "$tag" \ + "${assets[@]}" \ + --title "$tag" \ + --generate-notes \ + $prerelease_flag + fi diff --git a/.gitignore b/.gitignore index 0808c4a..d83b6fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,482 +1,484 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from `dotnet new gitignore` - -# dotenv files -.env - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET -project.lock.json -project.fragment.lock.json -artifacts/ - -# Tye -.tye/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -# but not Directory.Build.rsp, as it configures directory-level build defaults -!Directory.Build.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml -.idea/ - -## -## Visual studio for Mac -## - - -# globs -Makefile.in -*.userprefs -*.usertasks -config.make -config.status -aclocal.m4 -install-sh -autom4te.cache/ -*.tar.gz -tarballs/ -test-results/ - -# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore -# Windows thumbnail cache files -Thumbs.db -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# Vim temporary swap files -*.swp +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp + +coveragereport/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7648df2..68d7971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,34 @@ The format is based on Keep a Changelog. ## [Unreleased] +## [2.0.0] - 2026-06-02 + +### Added + +- **Esolang.Processor.Abstractions**: + - Introduced `IEventProcessor` for a unified, event-driven execution model. + - Added `IOEvent` and its subtypes (`InputChar`, `InputInt`, `OutputChar`, `OutputInt`, `End`) to represent I/O operations. + - Added `IProcessor` as a non-generic base interface. +- **Esolang.Generator.Abstractions**: + - Added comprehensive abstractions for method signature binding and type resolution. + - Introduced `MethodSignatureBinder` for mapping esolang source to C# partial methods. + - Added `MethodInputKind`, `MethodOutputKind`, and `MethodReturnKind` for signature classification. +- **Esolang.Interpreter.Abstractions**: + - Added new project for common interpreter utilities. + - Introduced `RunToConsoleAsync` extension method for running processors with standard console I/O. +- **Esolang.Processor.Extensions.IO**: + - Extracted and standardized I/O extension methods into a dedicated project. + - Added support for `TextReader`/`TextWriter`, `string`/`StringBuilder`, and `System.IO.Pipelines` (`PipeReader`/`PipeWriter`). +- Enhanced testing infrastructure across all abstraction projects, significantly improving code coverage. + +### Changed + +- **Esolang.Processor.Abstractions** (Breaking Changes): + - Removed `ITextProcessor` and `IPipeProcessor` interfaces in favor of the event-driven `IEventProcessor`. + - Modernized interfaces to use `IAsyncEnumerable` for execution. + - Standardized naming and namespaces. +- Updated `.editorconfig` with stricter C# style and MSTest diagnostic rules. + ## [1.0.0] - 2026-05-07 ### Added @@ -16,3 +44,7 @@ The format is based on Keep a Changelog. - `IPipeProcessor` — Execution interface using `PipeReader` and `PipeWriter` for high-performance pipe-based I/O. - Both text and pipe processors return exit codes (`int`) from execution. - Support for optional `CancellationToken` on all execution methods. + +[Unreleased]: https://github.com/Esolang-NET/Abstractions/compare/v2.0.0...HEAD +[2.0.0]: https://github.com/Esolang-NET/Abstractions/tree/v2.0.0 +[1.0.0]: https://github.com/Esolang-NET/Abstractions/tree/v1.0.0 diff --git a/Directory.Build.props b/Directory.Build.props index 4d954bc..57569ca 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,9 +3,9 @@ enable enable 14 - 1.0.0.1 - 1.0.0.1 - 1.0.0 + 2.0.0.2 + 2.0.0.2 + 2.0.0 https://github.com/Esolang-NET/Abstractions/ https://github.com/Esolang-NET/Abstractions.git true @@ -16,6 +16,7 @@ $(NoWarn);NETSDK1213;CS9057 snupkg True + true @@ -36,11 +37,19 @@ + + + + + $(MSBuildProjectDirectory)\TestResults false true false - $(NoWarn);RS1035 + $(NoWarn);RS1035;CS1591 + Exe + true + true diff --git a/Directory.Build.targets b/Directory.Build.targets index 18aa81f..86613d0 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/Esolang.Abstractions.code-workspace b/Esolang.Abstractions.code-workspace index 1369a7f..34e7e66 100644 --- a/Esolang.Abstractions.code-workspace +++ b/Esolang.Abstractions.code-workspace @@ -4,9 +4,33 @@ "path": ".", "name": "root" }, + { + "path": "Generator.Abstractions", + "name": "Esolang.Generator.Abstractions" + }, + { + "path": "Generator.Abstractions.Tests", + "name": "Esolang.Generator.Abstractions.Tests" + }, + { + "path": "Interpreter.Abstractions", + "name": "Esolang.Interpreter.Abstractions" + }, + { + "path": "Interpreter.Abstractions.Tests", + "name": "Esolang.Interpreter.Abstractions.Tests" + }, { "path": "Processor.Abstractions", "name": "Esolang.Processor.Abstractions" + }, + { + "path": "Processor.Extensions.IO", + "name": "Esolang.Processor.Extensions.IO" + }, + { + "path": "Processor.Extensions.IO.Tests", + "name": "Esolang.Processor.Extensions.IO.Tests" } ], "settings": { @@ -16,7 +40,14 @@ "**/.hg": true, "**/.DS_Store": true, "**/Thumbs.db": true, + "Generator.Abstractions": true, + "Generator.Abstractions.Tests": true, + "Interpreter.Abstractions": true, + "Interpreter.Abstractions.Tests": true, "Processor.Abstractions": true, + "Processor.Abstractions.Tests": true, + "Processor.Extensions.IO": true, + "Processor.Extensions.IO.Tests": true } } } \ No newline at end of file diff --git a/Esolang.Abstractions.slnx b/Esolang.Abstractions.slnx index 9a64952..e275f57 100644 --- a/Esolang.Abstractions.slnx +++ b/Esolang.Abstractions.slnx @@ -10,5 +10,17 @@ + + + + + + + + + - \ No newline at end of file + + + + diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..e48be57 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,18 @@ +# Workflow Guidelines + +- **Working Directory**: Always ensure the current working directory is the solution root (containing `global.json` and the `.slnx` solution file) before executing `dotnet` commands. This is critical for correct SDK version resolution and dependency management. + +### Collecting Code Coverage + +To run tests and collect code coverage: + +```bash +dotnet test --coverage --coverage-output-format cobertura +``` + +To generate an HTML coverage report using ReportGenerator: + +```bash +dotnet reportgenerator "-reports:**/*.cobertura.xml" "-targetdir:coveragereport" -reporttypes:Html +``` +The report will be generated in the `coveragereport` directory. diff --git a/Generator.Abstractions.Tests/Esolang.Generator.Abstractions.Tests.csproj b/Generator.Abstractions.Tests/Esolang.Generator.Abstractions.Tests.csproj new file mode 100644 index 0000000..f6d2571 --- /dev/null +++ b/Generator.Abstractions.Tests/Esolang.Generator.Abstractions.Tests.csproj @@ -0,0 +1,38 @@ + + + + net48;net9.0;net10.0 + net9.0;net10.0 + Esolang.Generator.Tests + Esolang.Generator.Abstractions.Tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Generator.Abstractions.Tests/KindTests.cs b/Generator.Abstractions.Tests/KindTests.cs new file mode 100644 index 0000000..516026c --- /dev/null +++ b/Generator.Abstractions.Tests/KindTests.cs @@ -0,0 +1,47 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Esolang.Generator.Tests; + +[TestClass] +public class KindTests +{ + [TestMethod] + [SuppressMessage("MSTest", "MSTEST0032")] + public void MethodInputKind_HasExpectedValues() + { + Assert.AreEqual(0, (int)MethodInputKind.None); + Assert.AreEqual(1, (int)MethodInputKind.String); + Assert.AreEqual(2, (int)MethodInputKind.TextReader); + Assert.AreEqual(3, (int)MethodInputKind.PipeReader); + } + + [TestMethod] + [SuppressMessage("MSTest", "MSTEST0032")] + public void MethodOutputKind_HasExpectedValues() + { + Assert.AreEqual(0, (int)MethodOutputKind.None); + Assert.AreEqual(1, (int)MethodOutputKind.TextWriter); + Assert.AreEqual(2, (int)MethodOutputKind.PipeWriter); + } + + [TestMethod] + [SuppressMessage("MSTest", "MSTEST0032")] + public void MethodReturnKind_HasExpectedValues() + { + Assert.AreEqual(0, (int)MethodReturnKind.Invalid); + Assert.AreEqual(1, (int)MethodReturnKind.Void); + Assert.AreEqual(2, (int)MethodReturnKind.Int32); + Assert.AreEqual(3, (int)MethodReturnKind.String); + Assert.AreEqual(4, (int)MethodReturnKind.NullableString); + Assert.AreEqual(5, (int)MethodReturnKind.Task); + Assert.AreEqual(6, (int)MethodReturnKind.TaskInt32); + Assert.AreEqual(7, (int)MethodReturnKind.TaskString); + Assert.AreEqual(8, (int)MethodReturnKind.TaskNullableString); + Assert.AreEqual(9, (int)MethodReturnKind.ValueTask); + Assert.AreEqual(10, (int)MethodReturnKind.ValueTaskInt32); + Assert.AreEqual(11, (int)MethodReturnKind.ValueTaskString); + Assert.AreEqual(12, (int)MethodReturnKind.ValueTaskNullableString); + Assert.AreEqual(13, (int)MethodReturnKind.IEnumerableByte); + Assert.AreEqual(14, (int)MethodReturnKind.IAsyncEnumerableByte); + } +} diff --git a/Generator.Abstractions.Tests/KnownTypesTests.cs b/Generator.Abstractions.Tests/KnownTypesTests.cs new file mode 100644 index 0000000..285697c --- /dev/null +++ b/Generator.Abstractions.Tests/KnownTypesTests.cs @@ -0,0 +1,166 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Esolang.Generator.Tests; + +[TestClass] +public class KnownTypesTests +{ + static Compilation CreateCompilation(string code) + { + var assemblies = new[] + { + typeof(object).Assembly, + typeof(System.Threading.Tasks.Task).Assembly, + typeof(System.Linq.Enumerable).Assembly + }.ToList(); +#if NET472_OR_GREATER + assemblies.Add(typeof(System.Threading.Tasks.ValueTask).Assembly); +#endif + var references = assemblies + .Select(a => MetadataReference.CreateFromFile(a.Location)) + .ToList(); + + return CSharpCompilation.Create("TestCompilation", + [CSharpSyntaxTree.ParseText(code)], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); + } + + [TestMethod] + public void Constructor_ResolvesSpecialTypes() + { + var compilation = CreateCompilation("class C {}"); + var knownTypes = new KnownTypes(compilation); + + Assert.IsNotNull(knownTypes.String); + Assert.AreEqual(SpecialType.System_String, knownTypes.String.SpecialType); + Assert.IsNotNull(knownTypes.Byte); + Assert.AreEqual(SpecialType.System_Byte, knownTypes.Byte.SpecialType); + Assert.IsNotNull(knownTypes.Int32); + Assert.AreEqual(SpecialType.System_Int32, knownTypes.Int32.SpecialType); + } + + [TestMethod] + public void IsByte_ChecksCorrectly() + { + var compilation = CreateCompilation("class C { byte b; }"); + var knownTypes = new KnownTypes(compilation); + var byteType = compilation.GetSpecialType(SpecialType.System_Byte); + var intType = compilation.GetSpecialType(SpecialType.System_Int32); + + Assert.IsTrue(knownTypes.IsByte(byteType)); + Assert.IsFalse(knownTypes.IsByte(intType)); + Assert.IsFalse(knownTypes.IsByte(null)); + } + + [TestMethod] + public void IsInt32_ChecksCorrectly() + { + var compilation = CreateCompilation("class C { int i; }"); + var knownTypes = new KnownTypes(compilation); + var intType = compilation.GetSpecialType(SpecialType.System_Int32); + var byteType = compilation.GetSpecialType(SpecialType.System_Byte); + + Assert.IsTrue(knownTypes.IsInt32(intType)); + Assert.IsFalse(knownTypes.IsInt32(byteType)); + Assert.IsFalse(knownTypes.IsInt32(null)); + } + + [TestMethod] + public void IsTask_ChecksCorrectly() + { + var compilation = CreateCompilation("using System.Threading.Tasks; class C { Task T() => Task.CompletedTask; }"); + var knownTypes = new KnownTypes(compilation); + var taskType = knownTypes.Task; + + Assert.IsNotNull(taskType); + Assert.IsTrue(knownTypes.IsTask(taskType)); + Assert.IsFalse(knownTypes.IsTask(compilation.GetSpecialType(SpecialType.System_String))); + Assert.IsFalse(knownTypes.IsTask(null)); + } + + [TestMethod] + public void IsString_NullableEnabledChecksCorrectly() + { + var compilation = CreateCompilation(""" + #nullable enable + class C { string? s; } + """); + var knownTypes = new KnownTypes(compilation); + var classC = compilation.GetTypeByMetadataName("C"); + var field = classC?.GetMembers("s").OfType().FirstOrDefault(); + + Assert.IsNotNull(field); + Assert.IsTrue(knownTypes.IsString(field.Type, isNullable: true)); + Assert.IsFalse(knownTypes.IsString(field.Type, isNullable: false)); + } + + [TestMethod] + public void IsString_NullableDisabledChecksCorrectly() + { + var compilation = CreateCompilation(""" + #nullable disable + class C { string s; } + """); + var knownTypes = new KnownTypes(compilation); + var classC = compilation.GetTypeByMetadataName("C"); + var field = classC?.GetMembers("s").OfType().FirstOrDefault(); + + Assert.IsNotNull(field); + Assert.IsTrue(knownTypes.IsString(field.Type, isNullable: false)); + Assert.IsFalse(knownTypes.IsString(field.Type, isNullable: true)); + } + + [TestMethod] + public void IsTaskT_NullableChecksCorrectly() + { + var compilation = CreateCompilation(""" + #nullable enable + using System.Threading.Tasks; + class C { + Task T1() => Task.FromResult(""); + Task T2() => Task.FromResult(null); + } + """); + var knownTypes = new KnownTypes(compilation); + var classC = compilation.GetTypeByMetadataName("C"); + var method1 = classC?.GetMembers("T1").OfType().FirstOrDefault(); + var method2 = classC?.GetMembers("T2").OfType().FirstOrDefault(); + + Assert.IsNotNull(method1); + Assert.IsNotNull(method2); + + Assert.IsTrue(knownTypes.IsTaskT(method1.ReturnType, isNullable: false)); + Assert.IsFalse(knownTypes.IsTaskT(method1.ReturnType, isNullable: true)); + + Assert.IsTrue(knownTypes.IsTaskT(method2.ReturnType, isNullable: true)); + Assert.IsFalse(knownTypes.IsTaskT(method2.ReturnType, isNullable: false)); + } + + [TestMethod] + public void IsValueTaskT_NullableChecksCorrectly() + { + var compilation = CreateCompilation(""" + #nullable enable + using System.Threading.Tasks; + class C { + ValueTask T1() => new ValueTask(""); + ValueTask T2() => new ValueTask(null); + } + """); + var knownTypes = new KnownTypes(compilation); + var classC = compilation.GetTypeByMetadataName("C"); + var method1 = classC?.GetMembers("T1").OfType().FirstOrDefault(); + var method2 = classC?.GetMembers("T2").OfType().FirstOrDefault(); + + Assert.IsNotNull(method1); + Assert.IsNotNull(method2); + + Assert.IsTrue(knownTypes.IsValueTaskT(method1.ReturnType, isNullable: false)); + Assert.IsFalse(knownTypes.IsValueTaskT(method1.ReturnType, isNullable: true)); + + Assert.IsTrue(knownTypes.IsValueTaskT(method2.ReturnType, isNullable: true)); + Assert.IsFalse(knownTypes.IsValueTaskT(method2.ReturnType, isNullable: false)); + } +} diff --git a/Generator.Abstractions.Tests/MethodSignatureBinderTests.cs b/Generator.Abstractions.Tests/MethodSignatureBinderTests.cs new file mode 100644 index 0000000..f56fd45 --- /dev/null +++ b/Generator.Abstractions.Tests/MethodSignatureBinderTests.cs @@ -0,0 +1,382 @@ +using Basic.Reference.Assemblies; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Esolang.Generator.Tests; + +[TestClass] +public class MethodSignatureBinderTests +{ + readonly TestContext TestContext; + readonly Compilation baseCompilation = default!; + + CancellationToken CancellationToken => TestContext.CancellationToken; + + public MethodSignatureBinderTests(TestContext TestContext) + { + this.TestContext = TestContext; + IEnumerable references = +#if NET10_0_OR_GREATER + Net100.References.All; +#elif NET9_0_OR_GREATER + Net90.References.All; +#elif NET472_OR_GREATER + Net472.References.All; +#else + throw new InvalidOperationException("Unsupported target framework for generator tests."); +#endif + + var referenceList = references.ToList(); + var extraTypes = new[] { + typeof(System.IO.Pipelines.PipeReader), + typeof(Microsoft.Extensions.Logging.ILogger), +#if NET472_OR_GREATER + typeof(ValueTask), + typeof(IAsyncEnumerable<>) +#endif + }; + + foreach (var type in extraTypes) + { + var location = type.Assembly.Location; + if (!string.IsNullOrWhiteSpace(location) && !referenceList.Any(r => Path.GetFileName(r.FilePath) == Path.GetFileName(location))) + { + referenceList.Add(MetadataReference.CreateFromFile(location)); + } + } + + baseCompilation = CSharpCompilation.Create("generatortest", + references: referenceList, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); + } + + (IMethodSymbol, Compilation) GetMethodAndCompilation(string code, string methodName) + { + var tree = CSharpSyntaxTree.ParseText("#nullable enable\n" + code, cancellationToken: CancellationToken); + var compilation = baseCompilation.AddSyntaxTrees(tree); + var classC = compilation.GetTypeByMetadataName("C"); + Assert.IsNotNull(classC, "Failed to get symbol for class C"); + var methodSymbol = classC.GetMembers(methodName).OfType().FirstOrDefault(); + Assert.IsNotNull(methodSymbol, $"Failed to get symbol for method {methodName}"); + return (methodSymbol, compilation); + } + + [TestMethod] + public void Bind_VoidMethod_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("class C { void M() {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid, $"binding: {binding}"); + Assert.AreEqual(MethodReturnKind.Void, binding.ReturnKind, $"binding: {binding}"); + } + + [TestMethod] + public void Bind_TaskMethod_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.Threading.Tasks; class C { Task M() => Task.CompletedTask; }", "M"); + var knownTypes = new KnownTypes(compilation); + + Assert.IsNotNull(knownTypes.Task, "KnownTypes.Task is null"); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid, $"binding: {binding}"); + Assert.AreEqual(MethodReturnKind.Task, binding.ReturnKind, $"binding: {binding}"); + } + + [TestMethod] + public void Bind_ValueTaskMethod_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.Threading.Tasks; class C { ValueTask M() => default; }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid, $"binding: {binding}"); + Assert.AreEqual(MethodReturnKind.ValueTask, binding.ReturnKind, $"binding: {binding}"); + } + + [TestMethod] + public void Bind_TaskTMethod_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.Threading.Tasks; class C { Task M() => Task.FromResult(0); }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid, $"binding: {binding}"); + Assert.AreEqual(MethodReturnKind.TaskInt32, binding.ReturnKind, $"binding: {binding}"); + } + + [TestMethod] + public void Bind_ValueTaskTMethod_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.Threading.Tasks; class C { ValueTask M() => default; }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid, $"binding: {binding}"); + Assert.AreEqual(MethodReturnKind.ValueTaskInt32, binding.ReturnKind, $"binding: {binding}"); + } + + [TestMethod] + public void Bind_StringInput_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("class C { void M(string s) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid, $"binding: {binding}"); + Assert.AreEqual(MethodInputKind.String, binding.InputKind); + Assert.AreEqual("s", binding.InputExpression); + } + + [TestMethod] + public void Bind_TextReaderInput_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.IO; class C { void M(TextReader r) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid, $"binding: {binding}"); + Assert.AreEqual(MethodInputKind.TextReader, binding.InputKind); + Assert.AreEqual("r", binding.InputExpression); + } + + [TestMethod] + public void Bind_TextWriterOutput_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.IO; class C { void M(TextWriter w) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid, $"binding: {binding}"); + Assert.AreEqual(MethodOutputKind.TextWriter, binding.OutputKind); + Assert.AreEqual("w", binding.OutputExpression); + } + + [TestMethod] + public void Bind_LoggerInField_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation(""" + using Microsoft.Extensions.Logging; + class C { + ILogger _logger; + C(ILogger logger) => _logger = logger; + void M() {} + } + """, "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid); + Assert.IsFalse(binding.IsLoggerFromParameter); + Assert.AreEqual("_logger", binding.LoggerExpression); + } + + [TestMethod] + public void Bind_LoggerInBaseClass_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation(""" + using Microsoft.Extensions.Logging; + class B { protected ILogger _logger; } + class C : B { void M() {} } + """, "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid); + Assert.IsFalse(binding.IsLoggerFromParameter); + Assert.AreEqual("_logger", binding.LoggerExpression); + } + + [TestMethod] + public void Bind_LoggerInConstructorParameter_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation(""" + using Microsoft.Extensions.Logging; + class C { + C(ILogger logger) { } + void M() {} + } + """, "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid); + Assert.IsTrue(binding.IsLoggerFromParameter); + Assert.AreEqual("logger", binding.LoggerExpression); + } + + [TestMethod] + public void Bind_CancellationToken_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.Threading; class C { void M(CancellationToken ct) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid); + Assert.AreEqual("ct", binding.CancellationTokenName); + } + + [TestMethod] + public void Bind_UnhandledParameters_AddedToUnhandledList() + { + var (method, compilation) = GetMethodAndCompilation("class C { void M(int i, string s) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid); + Assert.HasCount(1, binding.UnhandledParameters); + Assert.AreEqual("i", binding.UnhandledParameters[0].Name); + } + + [TestMethod] + public void Bind_Integrated_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation(""" + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + class C { + ILogger _logger; + C(ILogger logger) { _logger = logger; } + Task M(string input, CancellationToken ct) => Task.FromResult(0); + } + """, "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + + Assert.IsTrue(binding.IsValid, $"binding: {binding}"); + Assert.AreEqual(MethodReturnKind.TaskInt32, binding.ReturnKind); + Assert.AreEqual(MethodInputKind.String, binding.InputKind); + Assert.AreEqual("input", binding.InputExpression); + Assert.AreEqual("ct", binding.CancellationTokenName); + Assert.IsFalse(binding.IsLoggerFromParameter); + Assert.AreEqual("_logger", binding.LoggerExpression); + } + + [TestMethod] + public void Bind_InvalidReturnKind_ReturnsInvalidBinding() + { + var (method, compilation) = GetMethodAndCompilation("class C { float M() => 0.0f; }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsFalse(binding.IsValid); + Assert.AreEqual(BindingErrorKind.UnsupportedReturnType, binding.Error!.Kind); // invalidReturnTypeErrorId + } + + [TestMethod] + public void Bind_PipeReaderInput_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.IO.Pipelines; class C { void M(PipeReader r) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid); + Assert.AreEqual(MethodInputKind.PipeReader, binding.InputKind); + Assert.AreEqual("r", binding.InputExpression); + } + + [TestMethod] + public void Bind_PipeWriterOutput_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.IO.Pipelines; class C { void M(PipeWriter w) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid); + Assert.AreEqual(MethodOutputKind.PipeWriter, binding.OutputKind); + Assert.AreEqual("w", binding.OutputExpression); + } + + [TestMethod] + public void Bind_RefParameter_ReturnsInvalidBinding() + { + var (method, compilation) = GetMethodAndCompilation("class C { void M(ref string s) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsFalse(binding.IsValid); + Assert.AreEqual(BindingErrorKind.InvalidParameterModifier, binding.Error!.Kind); // invalidParameterErrorId + } + + [TestMethod] + public void Bind_DuplicateStringInput_ReturnsInvalidBinding() + { + var (method, compilation) = GetMethodAndCompilation("class C { void M(string s1, string s2) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsFalse(binding.IsValid); + Assert.AreEqual(BindingErrorKind.DuplicateInput, binding.Error!.Kind); // DuplicateParameterErrorId + } + + [TestMethod] + public void Bind_DuplicateTextReaderInput_ReturnsInvalidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.IO; class C { void M(TextReader r1, TextReader r2) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsFalse(binding.IsValid); + Assert.AreEqual(BindingErrorKind.DuplicateInput, binding.Error!.Kind); // DuplicateParameterErrorId + } + + [TestMethod] + public void Bind_DuplicateTextWriterOutput_ReturnsInvalidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.IO; class C { void M(TextWriter w1, TextWriter w2) {} }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsFalse(binding.IsValid); + Assert.AreEqual(BindingErrorKind.DuplicateOutput, binding.Error!.Kind); // DuplicateParameterErrorId + } + + [TestMethod] + public void Bind_ReturnOutputConflict_ReturnsInvalidBinding() + { + var (method, compilation) = GetMethodAndCompilation("using System.IO; class C { string M(TextWriter w) => \"\"; }", "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsFalse(binding.IsValid); + Assert.AreEqual(BindingErrorKind.ReturnOutputConflict, binding.Error!.Kind); // ReturnOutputConflictErrorId + } + + [TestMethod] + public void Bind_DuplicateLoggerParameter_ReturnsInvalidBinding() + { + var (method, compilation) = GetMethodAndCompilation(""" + using Microsoft.Extensions.Logging; + class C { + void M(ILogger l1, ILogger l2) {} + } + """, "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsFalse(binding.IsValid); + Assert.AreEqual(BindingErrorKind.DuplicateLogger, binding.Error!.Kind); // DuplicateParameterErrorId + } + + [TestMethod] + public void Bind_LoggerInField_CanBeReferencedByName_ReturnsValidBinding() + { + var (method, compilation) = GetMethodAndCompilation(""" + using Microsoft.Extensions.Logging; + class C { + public ILogger loggerField; + void M() {} + } + """, "M"); + var knownTypes = new KnownTypes(compilation); + + var binding = MethodSignatureBinder.Bind(method, knownTypes); + Assert.IsTrue(binding.IsValid); + Assert.IsFalse(binding.IsLoggerFromParameter); + Assert.AreEqual("loggerField", binding.LoggerExpression); + } +} diff --git a/Generator.Abstractions.Tests/MethodSignatureBindingTests.cs b/Generator.Abstractions.Tests/MethodSignatureBindingTests.cs new file mode 100644 index 0000000..3400ada --- /dev/null +++ b/Generator.Abstractions.Tests/MethodSignatureBindingTests.cs @@ -0,0 +1,35 @@ +namespace Esolang.Generator.Tests; + +[TestClass] +public class MethodSignatureBindingTests +{ + [TestMethod] + public void Properties_CheckCorrectly() + { + // IsAsync: Task 系 + var bindingTask = new MethodSignatureBinding(MethodReturnKind.Task, MethodInputKind.None, MethodOutputKind.None, "", "", null, null, false, []); + Assert.IsTrue(bindingTask.IsAsync); + + // IsAsync: ValueTask 系 + var bindingValueTask = new MethodSignatureBinding(MethodReturnKind.ValueTask, MethodInputKind.None, MethodOutputKind.None, "", "", null, null, false, []); + Assert.IsTrue(bindingValueTask.IsAsync); + + // IsEnumerable + var bindingEnum = new MethodSignatureBinding(MethodReturnKind.IEnumerableByte, MethodInputKind.None, MethodOutputKind.None, "", "", null, null, false, []); + Assert.IsTrue(bindingEnum.IsEnumerable); + Assert.IsFalse(bindingEnum.IsAsync); + + // IsAsyncEnumerable + var bindingAsyncEnum = new MethodSignatureBinding(MethodReturnKind.IAsyncEnumerableByte, MethodInputKind.None, MethodOutputKind.None, "", "", null, null, false, []); + Assert.IsTrue(bindingAsyncEnum.IsAsyncEnumerable); + Assert.IsTrue(bindingAsyncEnum.IsAsync); + + // HasExplicitInput + var bindingInput = new MethodSignatureBinding(MethodReturnKind.Void, MethodInputKind.String, MethodOutputKind.None, "s", "", null, null, false, []); + Assert.IsTrue(bindingInput.HasExplicitInput); + + // HasExplicitOutput + var bindingOutput = new MethodSignatureBinding(MethodReturnKind.Void, MethodInputKind.None, MethodOutputKind.TextWriter, "", "w", null, null, false, []); + Assert.IsTrue(bindingOutput.HasExplicitOutput); + } +} diff --git a/Generator.Abstractions.Tests/NullableTest.cs b/Generator.Abstractions.Tests/NullableTest.cs new file mode 100644 index 0000000..ee9e01f --- /dev/null +++ b/Generator.Abstractions.Tests/NullableTest.cs @@ -0,0 +1,23 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Esolang.Generator.Tests; + +[TestClass] +public class NullableTest(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 CheckNullableContext() + { + var compilation = CSharpCompilation.Create("Test", + [CSharpSyntaxTree.ParseText("class C {}", cancellationToken: CancellationToken)], + null, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + // デフォルトは Disable であるはず + Assert.AreEqual(NullableContextOptions.Disable, compilation.Options.NullableContextOptions); + } +} diff --git a/Generator.Abstractions/BindingError.cs b/Generator.Abstractions/BindingError.cs new file mode 100644 index 0000000..e3823c1 --- /dev/null +++ b/Generator.Abstractions/BindingError.cs @@ -0,0 +1,107 @@ +using Microsoft.CodeAnalysis; + +namespace Esolang.Generator; + +/// +/// Specifies the kind of diagnostic error that occurred during method signature binding. +/// +public enum BindingErrorKind +{ + /// The return type of the method is not supported. + UnsupportedReturnType, + /// A parameter has an invalid modifier (e.g., ref, out, in). + InvalidParameterModifier, + /// More than one parameter is competing for the same input role. + DuplicateInput, + /// More than one parameter is competing for the same output role. + DuplicateOutput, + /// More than one cancellation token parameter was found. + DuplicateCancellationToken, + /// More than one logger parameter was found. + DuplicateLogger, + /// A conflict exists between the return type and an output parameter. + ReturnOutputConflict, +} + +/// +/// Represents a diagnostic error that occurred during method signature binding. +/// +public abstract record BindingError +{ + /// + /// + /// + /// The kind of error. + /// The location associated with the error. + BindingError(BindingErrorKind Kind, Location? Location) : base() + => (this.Kind, this.Location) = (Kind, Location); + + /// + /// The kind of error. + /// + public BindingErrorKind Kind { get; } + + /// + /// The location associated with the error. + /// + public Location? Location { get; } + + /// + /// The return type of the method is not supported. + /// + /// The unsupported return type symbol. + /// The location of the return type. + public sealed record UnsupportedReturnType(ITypeSymbol ReturnType, Location? Location) + : BindingError(BindingErrorKind.UnsupportedReturnType, Location); + + /// + /// A parameter has an invalid modifier (e.g., ref, out, in). + /// + /// The parameter with the invalid modifier. + /// The location of the parameter. + public sealed record InvalidParameterModifier(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.InvalidParameterModifier, Location); + + /// + /// More than one parameter is competing for the same input role. + /// + /// The parameter that caused the duplication. + /// The kind of input that was already assigned. + /// The location of the duplicate parameter. + public sealed record DuplicateInput(IParameterSymbol Parameter, MethodInputKind ExistingKind, Location? Location) + : BindingError(BindingErrorKind.DuplicateInput, Location); + + /// + /// More than one parameter is competing for the same output role. + /// + /// The parameter that caused the duplication. + /// The kind of output that was already assigned. + /// The location of the duplicate parameter. + public sealed record DuplicateOutput(IParameterSymbol Parameter, MethodOutputKind ExistingKind, Location? Location) + : BindingError(BindingErrorKind.DuplicateOutput, Location); + + /// + /// More than one cancellation token parameter was found. + /// + /// The duplicate cancellation token parameter. + /// The location of the duplicate parameter. + public sealed record DuplicateCancellationToken(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.DuplicateCancellationToken, Location); + + /// + /// More than one logger parameter was found. + /// + /// The duplicate logger parameter. + /// The location of the duplicate parameter. + public sealed record DuplicateLogger(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.DuplicateLogger, Location); + + /// + /// A conflict exists between the return type and an output parameter. + /// + /// The output parameter that conflicts with the return type. + /// The location of the conflicting parameter. + public sealed record ReturnOutputConflict(IParameterSymbol Parameter, Location? Location) + : BindingError(BindingErrorKind.ReturnOutputConflict, Location); + +} diff --git a/Generator.Abstractions/Esolang.Generator.Abstractions.csproj b/Generator.Abstractions/Esolang.Generator.Abstractions.csproj new file mode 100644 index 0000000..9c4d9ac --- /dev/null +++ b/Generator.Abstractions/Esolang.Generator.Abstractions.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0;netstandard2.1 + Esolang.Generator + true + Esolang.Generator.Abstractions + Common abstractions and binder utilities for implementing esolang code generators and Roslyn-based source generators. + README.md + esolang;generator;sourcegenerator;abstractions;roslyn;binder + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + diff --git a/Generator.Abstractions/IsExternalInit.cs b/Generator.Abstractions/IsExternalInit.cs new file mode 100644 index 0000000..9c1044d --- /dev/null +++ b/Generator.Abstractions/IsExternalInit.cs @@ -0,0 +1,11 @@ +namespace System.Runtime.CompilerServices; + +#if !NET5_0_OR_GREATER +/// +/// Reserved to be used by the compiler for tracking metadata. +/// This class should not be used by developers in source code. +/// +static class IsExternalInit +{ +} +#endif diff --git a/Generator.Abstractions/KnownTypes.cs b/Generator.Abstractions/KnownTypes.cs new file mode 100644 index 0000000..376d83f --- /dev/null +++ b/Generator.Abstractions/KnownTypes.cs @@ -0,0 +1,247 @@ +using Microsoft.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Esolang.Generator; + +/// +/// Holds resolved type symbols for a compilation. +/// +/// +/// Initializes a new instance of the struct. +/// +/// The compilation to resolve types from. +public readonly struct KnownTypes(Compilation compilation) +{ + /// The string type symbol. + public readonly INamedTypeSymbol? String = compilation.GetSpecialType(SpecialType.System_String); + /// The byte type symbol. + public readonly INamedTypeSymbol? Byte = compilation.GetSpecialType(SpecialType.System_Byte); + /// The int type symbol. + public readonly INamedTypeSymbol? Int32 = compilation.GetSpecialType(SpecialType.System_Int32); + /// The System.Threading.Tasks.Task type symbol. + public readonly INamedTypeSymbol? Task = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task"); + /// The System.Threading.Tasks.Task{TResult} type symbol. + public readonly INamedTypeSymbol? TaskT = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1"); + /// The System.Threading.Tasks.ValueTask type symbol. + public readonly INamedTypeSymbol? ValueTask = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask"); + /// The System.Threading.Tasks.ValueTask{TResult} type symbol. + public readonly INamedTypeSymbol? ValueTaskT = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); + /// The System.Collections.Generic.IEnumerable{T} type symbol. + public readonly INamedTypeSymbol? IEnumerableT = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IEnumerable`1"); + /// The System.Collections.Generic.IAsyncEnumerable{T} type symbol. + public readonly INamedTypeSymbol? IAsyncEnumerableT = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1"); + /// The System.IO.Pipelines.PipeReader type symbol. + public readonly INamedTypeSymbol? PipeReader = compilation.GetBestTypeByMetadataName("System.IO.Pipelines.PipeReader"); + /// The System.IO.Pipelines.PipeWriter type symbol. + public readonly INamedTypeSymbol? PipeWriter = compilation.GetBestTypeByMetadataName("System.IO.Pipelines.PipeWriter"); + /// The System.IO.TextReader type symbol. + public readonly INamedTypeSymbol? TextReader = compilation.GetBestTypeByMetadataName("System.IO.TextReader"); + /// The System.IO.TextWriter type symbol. + public readonly INamedTypeSymbol? TextWriter = compilation.GetBestTypeByMetadataName("System.IO.TextWriter"); + /// The System.Threading.CancellationToken type symbol. + public readonly INamedTypeSymbol? CancellationToken = compilation.GetBestTypeByMetadataName("System.Threading.CancellationToken"); + /// The Microsoft.Extensions.Logging.ILogger type symbol. + public readonly INamedTypeSymbol? ILogger = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger"); + /// The Microsoft.Extensions.Logging.ILogger{T} type symbol. + public readonly INamedTypeSymbol? ILoggerT = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger`1"); + + static bool EqualsDefinition(ITypeSymbol? type, ISymbol? symbol) => + type != null && symbol != null && SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, symbol); + + static bool EqualsType(ITypeSymbol? type, ISymbol? symbol) => + type != null && symbol != null && SymbolEqualityComparer.Default.Equals(type, symbol); + + /// Gets a value indicating whether the type is string. + /// The type to check. + /// Optional: Whether to check for nullability. + public readonly bool IsString(ITypeSymbol? type, bool? isNullable = null) + { + if (type is not INamedTypeSymbol named || !SymbolEqualityComparer.Default.Equals(named, String)) return false; + if (isNullable == null) return true; + if (isNullable.Value) return type.NullableAnnotation == NullableAnnotation.Annotated; + return type.NullableAnnotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; + } + + /// Gets a value indicating whether the type is byte. + public readonly bool IsByte(ITypeSymbol? type) => EqualsType(type, Byte); + /// Gets a value indicating whether the type is int. + public readonly bool IsInt32(ITypeSymbol? type) => EqualsType(type, Int32); + + /// Gets a value indicating whether the type is System.Threading.Tasks.Task. + public readonly bool IsTask(ITypeSymbol? type) => EqualsType(type, Task); + /// Gets a value indicating whether the type is System.Threading.Tasks.Task{TResult}. + public readonly bool IsTaskT(ITypeSymbol? type, bool? isNullable = null) + { + if (type is not INamedTypeSymbol named || !EqualsDefinition(named, TaskT)) return false; + if (isNullable == null) return true; + var annotation = named.TypeArguments[0].NullableAnnotation; + return isNullable.Value ? annotation == NullableAnnotation.Annotated : annotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; + } + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask. + public readonly bool IsValueTask(ITypeSymbol? type) => EqualsType(type, ValueTask); + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{TResult}. + public readonly bool IsValueTaskT(ITypeSymbol? type, bool? isNullable = null) + { + if (type is not INamedTypeSymbol named || !EqualsDefinition(named, ValueTaskT)) return false; + if (isNullable == null) return true; + var annotation = named.TypeArguments[0].NullableAnnotation; + return isNullable.Value ? annotation == NullableAnnotation.Annotated : annotation is NullableAnnotation.NotAnnotated or NullableAnnotation.None; + } + /// Gets a value indicating whether the type is System.Collections.Generic.IEnumerable{T}. + public readonly bool IsIEnumerableT(ITypeSymbol? type) => EqualsDefinition(type, IEnumerableT); + /// Gets a value indicating whether the type is System.Collections.Generic.IAsyncEnumerable{T}. + public readonly bool IsIAsyncEnumerableT(ITypeSymbol? type) => EqualsDefinition(type, IAsyncEnumerableT); + + /// Gets a value indicating whether the type is System.IO.Pipelines.PipeReader. + public readonly bool IsPipeReader(ITypeSymbol? type) => EqualsType(type, PipeReader); + /// Gets a value indicating whether the type is System.IO.Pipelines.PipeWriter. + public readonly bool IsPipeWriter(ITypeSymbol? type) => EqualsType(type, PipeWriter); + /// Gets a value indicating whether the type is System.IO.TextReader. + public readonly bool IsTextReader(ITypeSymbol? type) => EqualsType(type, TextReader); + /// Gets a value indicating whether the type is System.IO.TextWriter. + public readonly bool IsTextWriter(ITypeSymbol? type) => EqualsType(type, TextWriter); + /// Gets a value indicating whether the type is System.Threading.CancellationToken. + public readonly bool IsCancellationToken(ITypeSymbol? type) => EqualsType(type, CancellationToken); + + /// Gets a value indicating whether the type is System.Threading.Tasks.Task{String}. + public readonly bool IsTaskString(ITypeSymbol? type, bool? isNullable = null) => IsTaskT(type, isNullable) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_String; + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{String}. + public readonly bool IsValueTaskString(ITypeSymbol? type, bool? isNullable = null) => IsValueTaskT(type, isNullable) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_String; + + /// Gets a value indicating whether the type is System.Threading.Tasks.Task{Int32}. + public readonly bool IsTaskInt32(ITypeSymbol? type) => IsTaskT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Int32; + /// Gets a value indicating whether the type is System.Threading.Tasks.ValueTask{Int32}. + public readonly bool IsValueTaskInt32(ITypeSymbol? type) => IsValueTaskT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Int32; + + /// Gets a value indicating whether the type is System.Collections.Generic.IEnumerable{Byte}. + public readonly bool IsIEnumerableByte(ITypeSymbol? type) => IsIEnumerableT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Byte; + /// Gets a value indicating whether the type is System.Collections.Generic.IAsyncEnumerable{Byte}. + public readonly bool IsIAsyncEnumerableByte(ITypeSymbol? type) => IsIAsyncEnumerableT(type) && ((INamedTypeSymbol)type!).TypeArguments[0].SpecialType == SpecialType.System_Byte; + + /// Gets a value indicating whether the type is a logger type (ILogger or ILogger{T}). + public readonly bool IsLogger(ITypeSymbol? type) + { + if (type == null) return false; + if (EqualsType(type, ILogger) || EqualsDefinition(type, ILoggerT)) return true; + foreach (var iface in type.AllInterfaces) + { + if (EqualsType(iface, ILogger) || EqualsDefinition(iface, ILoggerT)) return true; + } + return false; + } + + [ExcludeFromCodeCoverage] + readonly bool PrintMembers(StringBuilder builder) + { + builder.Append(nameof(String)).Append('='); + AppendNamedTypeSymbol(String, builder); + builder.Append(", "); + + builder.Append(nameof(Byte)).Append('='); + AppendNamedTypeSymbol(Byte, builder); + builder.Append(", "); + + builder.Append(nameof(Int32)).Append('='); + AppendNamedTypeSymbol(Int32, builder); + builder.Append(", "); + + builder.Append(nameof(Task)).Append('='); + AppendNamedTypeSymbol(Task, builder); + builder.Append(", "); + + builder.Append(nameof(TaskT)).Append('='); + AppendNamedTypeSymbol(TaskT, builder); + builder.Append(", "); + + builder.Append(nameof(ValueTask)).Append('='); + AppendNamedTypeSymbol(ValueTask, builder); + builder.Append(", "); + + builder.Append(nameof(ValueTaskT)).Append('='); + AppendNamedTypeSymbol(ValueTaskT, builder); + builder.Append(", "); + + builder.Append(nameof(IEnumerableT)).Append('='); + AppendNamedTypeSymbol(IEnumerableT, builder); + builder.Append(", "); + + builder.Append(nameof(IAsyncEnumerableT)).Append('='); + AppendNamedTypeSymbol(IAsyncEnumerableT, builder); + builder.Append(", "); + + builder.Append(nameof(PipeReader)).Append('='); + AppendNamedTypeSymbol(PipeReader, builder); + builder.Append(", "); + + builder.Append(nameof(PipeWriter)).Append('='); + AppendNamedTypeSymbol(PipeWriter, builder); + builder.Append(", "); + + builder.Append(nameof(TextReader)).Append('='); + AppendNamedTypeSymbol(TextReader, builder); + builder.Append(", "); + + builder.Append(nameof(TextWriter)).Append('='); + AppendNamedTypeSymbol(TextWriter, builder); + builder.Append(", "); + + builder.Append(nameof(CancellationToken)).Append('='); + AppendNamedTypeSymbol(CancellationToken, builder); + builder.Append(", "); + + builder.Append(nameof(ILogger)).Append('='); + AppendNamedTypeSymbol(ILogger, builder); + builder.Append(", "); + + builder.Append(nameof(ILoggerT)).Append('='); + AppendNamedTypeSymbol(ILoggerT, builder); + + return true; + static void AppendNamedTypeSymbol(INamedTypeSymbol? symbol, StringBuilder builder) + { + if (symbol == null) return; + builder.Append('('); + builder.Append(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + builder.Append(", "); + builder.Append(nameof(symbol.NullableAnnotation)).Append('=').Append(symbol.NullableAnnotation); + builder.Append(')'); + } + } + + /// + [ExcludeFromCodeCoverage] + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append(nameof(KnownTypes)).Append(" {"); + if (!PrintMembers(builder)) + { + builder.Append(' '); + } + builder.Append('}'); + return builder.ToString(); + } +} + +/// +/// Provides utility methods for resolving types from a . +/// +public static class TypeResolutionExtensions +{ + /// + /// Resolves the best for the specified metadata name. + /// + public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, string metadataName) + { + var type = compilation.GetTypeByMetadataName(metadataName); + if (type != null) return type; + + foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols) + { + var found = assembly.GetTypeByMetadataName(metadataName); + if (found != null) return found; + } + return null; + } +} diff --git a/Generator.Abstractions/MethodInputKind.cs b/Generator.Abstractions/MethodInputKind.cs new file mode 100644 index 0000000..30bea45 --- /dev/null +++ b/Generator.Abstractions/MethodInputKind.cs @@ -0,0 +1,16 @@ +namespace Esolang.Generator; + +/// +/// Specifies the input mechanism of the generated method. +/// +public enum MethodInputKind +{ + /// No explicit input mechanism. + None, + /// Input is provided via a string parameter. + String, + /// Input is provided via a TextReader parameter. + TextReader, + /// Input is provided via a PipeReader parameter. + PipeReader, +} diff --git a/Generator.Abstractions/MethodOutputKind.cs b/Generator.Abstractions/MethodOutputKind.cs new file mode 100644 index 0000000..4d48f81 --- /dev/null +++ b/Generator.Abstractions/MethodOutputKind.cs @@ -0,0 +1,20 @@ +namespace Esolang.Generator; + +/// +/// Specifies the output mechanism of the generated method. +/// +public enum MethodOutputKind +{ + /// No explicit output mechanism. + None, + /// Output is written to a TextWriter parameter. + TextWriter, + /// Output is written to a PipeWriter parameter. + PipeWriter, + /// Output is returned as a string. + ReturnString, + /// Output is yielded via IEnumerable<byte>. + ReturnIEnumerable, + /// Output is yielded via IAsyncEnumerable<byte>. + ReturnIAsyncEnumerable, +} diff --git a/Generator.Abstractions/MethodReturnKind.cs b/Generator.Abstractions/MethodReturnKind.cs new file mode 100644 index 0000000..f2ae6f6 --- /dev/null +++ b/Generator.Abstractions/MethodReturnKind.cs @@ -0,0 +1,38 @@ +namespace Esolang.Generator; + +/// +/// Specifies the return type of the generated method. +/// +public enum MethodReturnKind +{ + /// The return type is invalid or unsupported. + Invalid, + /// The method returns void. + Void, + /// The method returns int. + Int32, + /// The method returns string. + String, + /// The method returns string (nullable). + NullableString, + /// The method returns Task. + Task, + /// The method returns Task<int>. + TaskInt32, + /// The method returns Task<string>. + TaskString, + /// The method returns Task<string?>. + TaskNullableString, + /// The method returns ValueTask. + ValueTask, + /// The method returns ValueTask<int>. + ValueTaskInt32, + /// The method returns ValueTask<string>. + ValueTaskString, + /// The method returns ValueTask<string?>. + ValueTaskNullableString, + /// The method returns IEnumerable<byte>. + IEnumerableByte, + /// The method returns IAsyncEnumerable<byte>. + IAsyncEnumerableByte, +} diff --git a/Generator.Abstractions/MethodSignatureBinder.cs b/Generator.Abstractions/MethodSignatureBinder.cs new file mode 100644 index 0000000..fe45969 --- /dev/null +++ b/Generator.Abstractions/MethodSignatureBinder.cs @@ -0,0 +1,225 @@ +using Microsoft.CodeAnalysis; +using static Esolang.Generator.BindingError; + +namespace Esolang.Generator; + +/// +/// Provides utility methods for binding method signatures to . +/// +public static class MethodSignatureBinder +{ + /// + /// Binds the specified method symbol to a . + /// + /// The method symbol to bind. + /// The known types for the compilation. + /// The result of the binding. + public static MethodSignatureBinding Bind( + IMethodSymbol method, + KnownTypes types) + { + var returnKind = BindReturnKind(method.ReturnType, types); + if (returnKind == MethodReturnKind.Invalid) + { + return new MethodSignatureBinding(returnKind, MethodInputKind.None, MethodOutputKind.None, "", "", null, null, false, method.Parameters, new UnsupportedReturnType(method.ReturnType, method.Locations.FirstOrDefault())); + } + + var outputKind = BindDefaultOutputKind(returnKind); + var inputKind = MethodInputKind.None; + var inputExpr = ""; + var outputExpr = ""; + string? cancellationTokenName = null; + string? loggerExpression = null; + var isLoggerFromParameter = false; + var unhandledParameters = new List(); + + foreach (var p in method.Parameters) + { + if (p.RefKind != RefKind.None) + { + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new InvalidParameterModifier(p, p.Locations.FirstOrDefault())); + } + + if (types.IsString(p.Type, false)) + { + if (inputKind != MethodInputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); + + inputKind = MethodInputKind.String; + inputExpr = p.Name; + continue; + } + + if (types.IsTextReader(p.Type)) + { + if (inputKind != MethodInputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); + + inputKind = MethodInputKind.TextReader; + inputExpr = p.Name; + continue; + } + + if (types.IsPipeReader(p.Type)) + { + if (inputKind != MethodInputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateInput(p, inputKind, p.Locations.FirstOrDefault())); + + inputKind = MethodInputKind.PipeReader; + inputExpr = p.Name; + continue; + } + + if (types.IsTextWriter(p.Type)) + { + if (IsOutputReturning(returnKind)) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new ReturnOutputConflict(p, p.Locations.FirstOrDefault())); + + if (outputKind != MethodOutputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateOutput(p, outputKind, p.Locations.FirstOrDefault())); + + outputKind = MethodOutputKind.TextWriter; + outputExpr = p.Name; + continue; + } + + if (types.IsPipeWriter(p.Type)) + { + if (IsOutputReturning(returnKind)) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new ReturnOutputConflict(p, p.Locations.FirstOrDefault())); + + if (outputKind != MethodOutputKind.None) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateOutput(p, outputKind, p.Locations.FirstOrDefault())); + + outputKind = MethodOutputKind.PipeWriter; + outputExpr = p.Name; + continue; + } + + if (types.IsCancellationToken(p.Type)) + { + if (cancellationTokenName != null) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateCancellationToken(p, p.Locations.FirstOrDefault())); + + cancellationTokenName = p.Name; + continue; + } + + if (types.IsLogger(p.Type)) + { + if (loggerExpression != null) + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, method.Parameters, new DuplicateLogger(p, p.Locations.FirstOrDefault())); + + loggerExpression = p.Name; + isLoggerFromParameter = true; + continue; + } + + unhandledParameters.Add(p); + } + + loggerExpression ??= FindLoggerInContainingType(method.ContainingType, method.IsStatic, types, out isLoggerFromParameter); + + return new MethodSignatureBinding(returnKind, inputKind, outputKind, inputExpr, outputExpr, cancellationTokenName, loggerExpression, isLoggerFromParameter, unhandledParameters); + } + + /// + /// Binds the return type symbol to a . + /// + public static MethodReturnKind BindReturnKind(ITypeSymbol returnType, KnownTypes types) + { + if (returnType.SpecialType == SpecialType.System_Void) return MethodReturnKind.Void; + if (returnType.SpecialType == SpecialType.System_Int32) return MethodReturnKind.Int32; + if (types.IsString(returnType, false)) return MethodReturnKind.String; + if (types.IsString(returnType, true)) return MethodReturnKind.NullableString; + if (types.IsTask(returnType)) return MethodReturnKind.Task; + if (types.IsTaskInt32(returnType)) return MethodReturnKind.TaskInt32; + if (types.IsTaskString(returnType, false)) return MethodReturnKind.TaskString; + if (types.IsTaskString(returnType, true)) return MethodReturnKind.TaskNullableString; + if (types.IsValueTask(returnType)) return MethodReturnKind.ValueTask; + if (types.IsValueTaskInt32(returnType)) return MethodReturnKind.ValueTaskInt32; + if (types.IsValueTaskString(returnType, false)) return MethodReturnKind.ValueTaskString; + if (types.IsValueTaskString(returnType, true)) return MethodReturnKind.ValueTaskNullableString; + if (types.IsIEnumerableByte(returnType)) return MethodReturnKind.IEnumerableByte; + if (types.IsIAsyncEnumerableByte(returnType)) return MethodReturnKind.IAsyncEnumerableByte; + + return MethodReturnKind.Invalid; + } + + /// + /// Gets the default output kind based on the return kind. + /// + static MethodOutputKind BindDefaultOutputKind(MethodReturnKind returnKind) => returnKind switch + { + MethodReturnKind.String or MethodReturnKind.NullableString or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString => MethodOutputKind.ReturnString, + MethodReturnKind.IEnumerableByte => MethodOutputKind.ReturnIEnumerable, + MethodReturnKind.IAsyncEnumerableByte => MethodOutputKind.ReturnIAsyncEnumerable, + _ => MethodOutputKind.None + }; + + /// + /// Gets a value indicating whether the return kind implies output is returned. + /// + static bool IsOutputReturning(MethodReturnKind returnKind) => returnKind switch + { + MethodReturnKind.String or MethodReturnKind.NullableString or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString or MethodReturnKind.IEnumerableByte or MethodReturnKind.IAsyncEnumerableByte => true, + _ => false + }; + + /// + /// Searches for a logger in the containing type (fields or constructor parameters). + /// + /// The type to search in. + /// Whether the target method is static. + /// The known types for the compilation. + /// Output: Whether the logger was found in a constructor parameter. + /// The expression to access the logger, or null if not found. + static string? FindLoggerInContainingType(ITypeSymbol? type, bool isStatic, KnownTypes types, out bool isFromParameter) + { + isFromParameter = false; + var currentType = type; + var shadowedNames = new HashSet(StringComparer.Ordinal); + var isBaseType = false; + + while (currentType != null) + { + foreach (var field in currentType.GetMembers().OfType()) + { + if (isStatic && !field.IsStatic) continue; + + // If searching in a base type, the field must be accessible (protected or public) + if (isBaseType && field.DeclaredAccessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal or Accessibility.Public or Accessibility.Internal)) + continue; + + if (types.IsLogger(field.Type)) + { + return field.Name; + } + + if (field.CanBeReferencedByName) + { + shadowedNames.Add(field.Name); + } + } + currentType = currentType.BaseType; + isBaseType = true; + } + + if (type is INamedTypeSymbol namedType) + { + foreach (var constructor in namedType.InstanceConstructors) + { + foreach (var parameter in constructor.Parameters) + { + if (types.IsLogger(parameter.Type) && !shadowedNames.Contains(parameter.Name)) + { + isFromParameter = true; + return parameter.Name; + } + } + } + } + + return null; + } +} diff --git a/Generator.Abstractions/MethodSignatureBinding.cs b/Generator.Abstractions/MethodSignatureBinding.cs new file mode 100644 index 0000000..fd59eef --- /dev/null +++ b/Generator.Abstractions/MethodSignatureBinding.cs @@ -0,0 +1,96 @@ +using Microsoft.CodeAnalysis; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Esolang.Generator; + +/// +/// Represents the result of binding a method signature for generation. +/// +/// The return kind of the method. +/// The input kind of the method. +/// The output kind of the method. +/// The expression to access the input (e.g., parameter name). +/// The expression to access the output (e.g., parameter name). +/// The name of the cancellation token parameter, if any. +/// The expression to access the logger (e.g., "loggerParam", "this._logger"). +/// Whether the logger is obtained from a method parameter. +/// Parameters that were not handled by the common binding logic. +/// The diagnostic error if the binding failed. +[DebuggerDisplay("{ToString(),nq}")] +public record struct MethodSignatureBinding( + MethodReturnKind ReturnKind, + MethodInputKind InputKind, + MethodOutputKind OutputKind, + string InputExpression, + string OutputExpression, + string? CancellationTokenName, + string? LoggerExpression, + bool IsLoggerFromParameter, + IReadOnlyList UnhandledParameters, + BindingError? Error = null) +{ + /// Whether the binding is successful. + [MemberNotNullWhen(false, nameof(Error))] + public readonly bool IsValid => Error is null; + + /// Gets a value indicating whether the method has an explicit input mechanism. + public readonly bool HasExplicitInput => InputKind != MethodInputKind.None; + + /// Gets a value indicating whether the method has an explicit output mechanism. + public readonly bool HasExplicitOutput => OutputKind != MethodOutputKind.None; + + /// Gets a value indicating whether the method is asynchronous. + public readonly bool IsAsync => ReturnKind switch + { + MethodReturnKind.Task or MethodReturnKind.TaskInt32 or MethodReturnKind.TaskString or MethodReturnKind.TaskNullableString or + MethodReturnKind.ValueTask or MethodReturnKind.ValueTaskInt32 or MethodReturnKind.ValueTaskString or MethodReturnKind.ValueTaskNullableString or + MethodReturnKind.IAsyncEnumerableByte => true, + _ => false + }; + + /// Gets a value indicating whether the method returns an enumerable. + public readonly bool IsEnumerable => ReturnKind == MethodReturnKind.IEnumerableByte; + + /// Gets a value indicating whether the method returns an async enumerable. + public readonly bool IsAsyncEnumerable => ReturnKind == MethodReturnKind.IAsyncEnumerableByte; + + [ExcludeFromCodeCoverage] + readonly bool PrintMembers(StringBuilder builder) + { + builder.Append(nameof(IsValid)).Append('=').Append(IsValid).Append(", "); + builder.Append(nameof(ReturnKind)).Append('=').Append(ReturnKind).Append(", "); + builder.Append(nameof(InputKind)).Append('=').Append(InputKind).Append(", "); + builder.Append(nameof(OutputKind)).Append('=').Append(OutputKind).Append(", "); + builder.Append(nameof(InputExpression)).Append('=').Append(InputExpression).Append(", "); + builder.Append(nameof(OutputExpression)).Append('=').Append(OutputExpression).Append(", "); + builder.Append(nameof(CancellationTokenName)).Append('=').Append(CancellationTokenName).Append(", "); + builder.Append(nameof(LoggerExpression)).Append('=').Append(LoggerExpression).Append(", "); + builder.Append(nameof(IsLoggerFromParameter)).Append('=').Append(IsLoggerFromParameter).Append(", "); + builder.Append(nameof(UnhandledParameters)).Append("=["); + for (var i = 0; i < UnhandledParameters.Count; i++) + { + if (i > 0) builder.Append(", "); + builder.Append(UnhandledParameters[i].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + builder.Append("], "); + builder.Append(nameof(Error)).Append('=').Append(Error); + return true; + } + + /// + [ExcludeFromCodeCoverage] + public override readonly string ToString() + { + var builder = new StringBuilder(); + builder.Append(nameof(MethodSignatureBinding)).Append(" {"); + if (!PrintMembers(builder)) + { + builder.Append(' '); + } + builder.Append('}'); + return builder.ToString(); + } + +} diff --git a/Generator.Abstractions/README.md b/Generator.Abstractions/README.md new file mode 100644 index 0000000..4c685fa --- /dev/null +++ b/Generator.Abstractions/README.md @@ -0,0 +1,27 @@ +# Esolang.Generator.Abstractions + +Common abstractions for esolang code generators. + +## Installation + +```bash +dotnet add package Esolang.Generator.Abstractions +``` + +## Overview + +This package provides common interfaces, types, and binder utilities for implementing esolang code generators and Roslyn-based source generators within the Esolang.NET ecosystem. + +## Key Components + +### MethodSignatureBinder + +The `MethodSignatureBinder` is a core utility that facilitates mapping esolang source code to C# partial method signatures. It handles the identification of input, output, and return patterns to generate appropriate boilerplate. + +### Binding Kinds + +To support diverse esolang execution models, several "Kind" enums are provided to classify method signatures: + +- **MethodInputKind**: Classifies how the esolang receives input (e.g., `TextReader`, `PipeReader`, `byte[]`, or none). +- **MethodOutputKind**: Classifies how the esolang sends output (e.g., `TextWriter`, `PipeWriter`, `StringBuilder`, or none). +- **MethodReturnKind**: Determines the method's return pattern (e.g., `void`, `string`, `int`, `Task`, `IEnumerable`, `IAsyncEnumerable`). diff --git a/Interpreter.Abstractions.Tests/Esolang.Interpreter.Abstractions.Tests.csproj b/Interpreter.Abstractions.Tests/Esolang.Interpreter.Abstractions.Tests.csproj new file mode 100644 index 0000000..c7fc2e9 --- /dev/null +++ b/Interpreter.Abstractions.Tests/Esolang.Interpreter.Abstractions.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + Esolang.Interpreter.Tests + Esolang.Interpreter.Abstractions.Tests + + + + + + + + + + + + + + + + + + + + diff --git a/Interpreter.Abstractions.Tests/InterpreterExtensionsTests.cs b/Interpreter.Abstractions.Tests/InterpreterExtensionsTests.cs new file mode 100644 index 0000000..2323018 --- /dev/null +++ b/Interpreter.Abstractions.Tests/InterpreterExtensionsTests.cs @@ -0,0 +1,52 @@ +using Esolang.Processor; +using static Esolang.Processor.IOEvent; + +namespace Esolang.Interpreter.Tests; + +[TestClass] +public class InterpreterExtensionsTests(TestContext TestContext) +{ + CancellationToken CancellationToken => TestContext.CancellationToken; + + [TestMethod] + [Timeout(2000, CooperativeCancellation = true)] + public async Task RunToConsoleAsync_ProcessesAllEvents() + { + var output = new StringWriter(); + Console.SetOut(output); + + var capturedChar = ' '; + var capturedInt = 0; + + var processor = new MockEventProcessor([ + OutputChar('A'), + OutputInt(123), + InputChar(c => capturedChar = c), + InputInt(i => capturedInt = i), + End(0) + ]); + + // Input simulation for interactive mode + using var input = new StringReader("B" + "456" + Environment.NewLine); + Console.SetIn(input); + + var exitCode = await processor.RunToConsoleAsync(cancellationToken: CancellationToken); + + Assert.AreEqual(0, exitCode); + Assert.AreEqual("A123", output.ToString()); + Assert.AreEqual('B', capturedChar); + Assert.AreEqual(456, capturedInt); + } + + class MockEventProcessor(List events) : IEventProcessor + { + public async IAsyncEnumerable RunAsyncEnumerable([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var ev in events) + { + yield return ev; + } + await Task.CompletedTask; + } + } +} diff --git a/Interpreter.Abstractions/Esolang.Interpreter.Abstractions.csproj b/Interpreter.Abstractions/Esolang.Interpreter.Abstractions.csproj new file mode 100644 index 0000000..346f679 --- /dev/null +++ b/Interpreter.Abstractions/Esolang.Interpreter.Abstractions.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + Common abstraction interfaces for implementing esolang interpreters, facilitating integration with the unified processor model. + Esolang.Interpreter.Abstractions + README.md + esolang;interpreter;abstractions;interface;execution;runtime + Esolang.Interpreter + + + + + + + + + + + + + diff --git a/Interpreter.Abstractions/InterpreterExtensions.cs b/Interpreter.Abstractions/InterpreterExtensions.cs new file mode 100644 index 0000000..8703575 --- /dev/null +++ b/Interpreter.Abstractions/InterpreterExtensions.cs @@ -0,0 +1,56 @@ +using Esolang.Processor; +using static Esolang.Processor.IOEvent; + +namespace Esolang.Interpreter; + +/// +/// Provides extension methods for running in an interpreter context. +/// +public static class InterpreterExtensions +{ + /// + /// Executes the processor using standard I/O (Console.In, Console.Out). + /// + /// The event processor. + /// The cancellation token. + /// The exit code. + public static async ValueTask RunToConsoleAsync( + this IEventProcessor processor, + CancellationToken cancellationToken = default) + { + await foreach (var ioEvent in processor.RunAsyncEnumerable(cancellationToken)) + { + switch (ioEvent) + { + case InputCharEvent charInput: + charInput.Write(await ReadCharFromConsoleAsync(cancellationToken)); + break; + case InputIntEvent intInput: + var line = await ReadLineFromConsoleAsync(cancellationToken); + if (int.TryParse(line, out var i)) + { + intInput.Write(i); + } + break; + case OutputCharEvent charOutput: + Console.Write(charOutput.Output); + break; + case OutputIntEvent intOutput: + Console.Write(intOutput.Output); + break; + case EndEvent end: + return end.ExitCode; + } + } + return 0; + } + + static async ValueTask ReadCharFromConsoleAsync(CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + var c = Console.In.Read(); + return c == -1 ? '\0' : (char)c; + } + + static async ValueTask ReadLineFromConsoleAsync(CancellationToken ct) => await Console.In.ReadLineAsync(ct); +} diff --git a/Interpreter.Abstractions/README.md b/Interpreter.Abstractions/README.md new file mode 100644 index 0000000..38bcda0 --- /dev/null +++ b/Interpreter.Abstractions/README.md @@ -0,0 +1,26 @@ +# Esolang.Interpreter.Abstractions + +Common abstractions for esolang interpreters. + +## Installation + +```bash +dotnet add package Esolang.Interpreter.Abstractions +``` + +## Overview + +This package provides common interfaces and extensions for implementing esolang interpreters within the Esolang.NET ecosystem. It focuses on facilitating the execution of any `IEventProcessor` using standard console I/O. + +## Usage + +### Run to Console + +The `RunToConsoleAsync` extension method allows you to execute an `IEventProcessor` directly using `Console.In` and `Console.Out`. + +```csharp +using Esolang.Interpreter; + +// Run your processor using standard I/O +int exitCode = await processor.RunToConsoleAsync(); +``` diff --git a/Processor.Abstractions/Esolang.Processor.Abstractions.csproj b/Processor.Abstractions/Esolang.Processor.Abstractions.csproj index 30ccc13..023ced3 100644 --- a/Processor.Abstractions/Esolang.Processor.Abstractions.csproj +++ b/Processor.Abstractions/Esolang.Processor.Abstractions.csproj @@ -1,16 +1,22 @@ - netstandard2.0 + netstandard2.0;netstandard2.1 enable true - Unified processor abstractions for esolang execution. + Core interfaces and unified abstractions for esolang execution models, providing event-driven I/O processing standards. Esolang.Processor.Abstractions README.md + esolang;processor;abstractions;interface;execution;event-driven;io + Esolang.Processor - + + + + + diff --git a/Processor.Abstractions/IOEvent.cs b/Processor.Abstractions/IOEvent.cs new file mode 100644 index 0000000..a0b3987 --- /dev/null +++ b/Processor.Abstractions/IOEvent.cs @@ -0,0 +1,106 @@ +namespace Esolang.Processor; + +/// +/// Represents an I/O event. +/// +public abstract class IOEvent +{ + IOEvent() { } + + /// + /// Creates an event requesting a character input. + /// + /// The action to write the input character to the processor. + /// An event requesting a character input. + public static InputCharEvent InputChar(Action write) => new(write); + + /// + /// Creates an event requesting an integer input. + /// + /// The action to write the input integer to the processor. + /// An event requesting an integer input. + public static InputIntEvent InputInt(Action write) => new(write); + + /// + /// Creates an event that outputs a character. + /// + /// The character to output. + /// An event that outputs a character. + public static OutputCharEvent OutputChar(char output) => new(output); + + /// + /// Creates an event that outputs an integer. + /// + /// The integer to output. + /// An event that outputs an integer. + public static OutputIntEvent OutputInt(int output) => new(output); + + /// + /// Creates an event indicating the end of execution. + /// + /// The exit code. + /// An event indicating the end of execution. + public static EndEvent End(int exitCode) => new(exitCode); + + /// + /// Represents an event requesting a character input. + /// + public sealed class InputCharEvent(Action write) : IOEvent + { + /// + /// Writes the input character to the processor. + /// + /// The input character. + public void Write(char c) => write(c); + } + + /// + /// Represents an event requesting an integer input. + /// + /// The action to write the input integer to the processor. + public sealed class InputIntEvent(Action write) : IOEvent + { + /// + /// Writes the input integer to the processor. + /// + /// The input integer. + public void Write(int i) => write(i); + } + + /// + /// Represents an event that outputs a character. + /// + /// The character to output. + public sealed class OutputCharEvent(char Output) : IOEvent + { + /// + /// The character to output. + /// + public char Output { get; } = Output; + } + + /// + /// Represents an event that outputs an integer. + /// + /// The integer to output. + public sealed class OutputIntEvent(int Output) : IOEvent + { + /// + /// The integer to output. + /// + public int Output { get; } = Output; + } + + /// + /// Represents an event indicating the end of execution. + /// + /// The exit code. + public sealed class EndEvent(int exitCode) : IOEvent + { + /// + /// The exit code. + /// + public int ExitCode { get; } = exitCode; + } + +} diff --git a/Processor.Abstractions/IProcessor.cs b/Processor.Abstractions/IProcessor.cs index ce93f7c..3ec3464 100644 --- a/Processor.Abstractions/IProcessor.cs +++ b/Processor.Abstractions/IProcessor.cs @@ -1,53 +1,30 @@ -using System.IO.Pipelines; - -#pragma warning disable IDE0130 // Namespace does not match folder structure namespace Esolang.Processor; -#pragma warning restore IDE0130 /// -/// Processor の共通基底。実行対象のプログラムを保持する。 +/// Common base interface for all processors. /// -/// パース済みプログラムの型。 -public interface IProcessor -{ - /// パース済みプログラム。 - TProgram Program { get; } -} +public interface IProcessor { } /// -/// / ベースの実行 IF。 +/// Common base interface for processors that hold a program to be executed. /// -/// パース済みプログラムの型。 -public interface ITextProcessor : IProcessor +/// The type of the parsed program. +public interface IProcessor : IProcessor { - /// プログラムを最後まで実行し、終了コードを返す。 - int RunToEnd( - TextReader? input = null, - TextWriter? output = null, - CancellationToken cancellationToken = default); - - /// プログラムを最後まで非同期実行し、終了コードを返す。 - ValueTask RunToEndAsync( - TextReader? input = null, - TextWriter? output = null, - CancellationToken cancellationToken = default); + /// The parsed program. + TProgram Program { get; } } /// -/// / ベースの実行 IF。 +/// Execution model based on a stream of I/O events. /// -/// パース済みプログラムの型。 -public interface IPipeProcessor : IProcessor +public interface IEventProcessor : IProcessor { - /// プログラムを最後まで実行し、終了コードを返す。 - int RunToEnd( - PipeReader input, - PipeWriter output, - CancellationToken cancellationToken = default); - - /// プログラムを最後まで非同期実行し、終了コードを返す。 - ValueTask RunToEndAsync( - PipeReader input, - PipeWriter output, + /// + /// Executes the processor and returns a stream of I/O events. + /// + /// The cancellation token. + /// An asynchronous stream of I/O events. + IAsyncEnumerable RunAsyncEnumerable( CancellationToken cancellationToken = default); } diff --git a/Processor.Abstractions/README.md b/Processor.Abstractions/README.md index 9b7f412..7f08594 100644 --- a/Processor.Abstractions/README.md +++ b/Processor.Abstractions/README.md @@ -10,105 +10,82 @@ dotnet add package Esolang.Processor.Abstractions ## Overview -This package provides common interfaces for implementing esolang processors (interpreters) with consistent execution patterns. +This package provides common interfaces and extension methods for implementing esolang processors (interpreters) based on an event-driven I/O model. -## Interfaces +## Core Interfaces ### IProcessor\ Base interface that holds a parsed program. ```csharp -public interface IProcessor +public interface IProcessor : IProcessor { - /// Parsed program instance. + /// The parsed program. TProgram Program { get; } } ``` -### ITextProcessor\ +### IEventProcessor -Execution interface using `TextReader` and `TextWriter` for input/output. +Execution interface based on a stream of I/O events. ```csharp -public interface ITextProcessor : IProcessor +public interface IEventProcessor : IProcessor { - /// Execute the program synchronously and return exit code. - int RunToEnd( - TextReader? input = null, - TextWriter? output = null, - CancellationToken cancellationToken = default); - - /// Execute the program asynchronously and return exit code. - ValueTask RunToEndAsync( - TextReader? input = null, - TextWriter? output = null, - CancellationToken cancellationToken = default); + /// + /// Executes the processor and returns an asynchronous stream of I/O events. + /// + IAsyncEnumerable RunAsyncEnumerable(CancellationToken cancellationToken = default); } ``` -### IPipeProcessor\ +## IO Events -Execution interface using `PipeReader` and `PipeWriter` for high-performance I/O. +The `IEventProcessor` communicates with the outside world through a stream of `IOEvent` objects. + +| Event Type | Purpose | Factory Method | +| --- | --- | --- | +| `InputCharEvent` | Requests a single character from the input. | `IOEvent.InputChar(Action write)` | +| `InputIntEvent` | Requests a single integer from the input. | `IOEvent.InputInt(Action write)` | +| `OutputCharEvent` | Sends a single character to the output. | `IOEvent.OutputChar(char output)` | +| `OutputIntEvent` | Sends a single integer to the output. | `IOEvent.OutputInt(int output)` | +| `EndEvent` | Signals the end of execution and provides an exit code. | `IOEvent.End(int exitCode)` | + +## Extension Methods + +To facilitate running processors, common extension methods are provided in separate packages: + +- **`Esolang.Processor.Extensions.IO`**: Contains `TextProcessorExtensions` (for `TextReader`/`TextWriter`), `StringProcessorExtensions` (for `string`/`StringBuilder`), and `PipeProcessorExtensions` (for `PipeReader`/`PipeWriter`). ```csharp -public interface IPipeProcessor : IProcessor -{ - /// Execute the program synchronously and return exit code. - int RunToEnd( - PipeReader input, - PipeWriter output, - CancellationToken cancellationToken = default); - - /// Execute the program asynchronously and return exit code. - ValueTask RunToEndAsync( - PipeReader input, - PipeWriter output, - CancellationToken cancellationToken = default); -} +// Example using TextReader/TextWriter +await processor.RunToEndAsync(inputReader, outputWriter, cancellationToken); + +// Example using PipeReader/PipeWriter +await processor.RunToEndAsync(inputPipe, outputPipe, cancellationToken); + +// Example using string input/output +var result = await processor.RunToStringAsync(input: "your_input", cancellationToken); ``` ## Usage Example -Implement these interfaces in your processor: +Implement `IEventProcessor` in your processor: ```csharp using Esolang.Processor; -using System.IO.Pipelines; -public class MyEsolangProcessor : ITextProcessor, IPipeProcessor +public class MyEsolangProcessor : IEventProcessor { public MyProgram Program { get; } - public MyEsolangProcessor(MyProgram program) - { - Program = program; - } - - // Text-based I/O - public int RunToEnd(TextReader? input = null, TextWriter? output = null, CancellationToken cancellationToken = default) + public async IAsyncEnumerable RunAsyncEnumerable(CancellationToken cancellationToken = default) { - // Execute program with text I/O, return exit code - return 0; - } - - public ValueTask RunToEndAsync(TextReader? input = null, TextWriter? output = null, CancellationToken cancellationToken = default) - { - // Async variant - return new ValueTask(0); - } - - // Pipe-based I/O - public int RunToEnd(PipeReader input, PipeWriter output, CancellationToken cancellationToken = default) - { - // Execute program with pipe I/O, return exit code - return 0; - } - - public ValueTask RunToEndAsync(PipeReader input, PipeWriter output, CancellationToken cancellationToken = default) - { - // Async variant - return new ValueTask(0); + // Implement the execution logic yielding IOEvents + yield return IOEvent.OutputChar('H'); + yield return IOEvent.OutputChar('i'); + yield return IOEvent.End(0); } } ``` @@ -116,10 +93,7 @@ public class MyEsolangProcessor : ITextProcessor, IPipeProcessor + + + net48;net9.0;net10.0 + net9.0;net10.0 + Esolang.Processor.Tests + Esolang.Processor.Extensions.IO.Tests + $(NoWarn);IDE0005 + + + + + + + + + + + + + + + + + + + + + diff --git a/Processor.Extensions.IO.Tests/PipeProcessorExtensionsTests.cs b/Processor.Extensions.IO.Tests/PipeProcessorExtensionsTests.cs new file mode 100644 index 0000000..6d33bb9 --- /dev/null +++ b/Processor.Extensions.IO.Tests/PipeProcessorExtensionsTests.cs @@ -0,0 +1,171 @@ +using Esolang.Processor; +using Esolang.Processor.Extensions.IO; +using System.Buffers; +using System.IO.Pipelines; +using System.Text; +using static Esolang.Processor.IOEvent; + +namespace Esolang.Processor.Tests; + +[TestClass] +public class PipeProcessorExtensionsTests(TestContext TestContext) +{ + CancellationToken CancellationToken => TestContext.CancellationToken; + + class MockEventProcessor(List events) : IEventProcessor + { + public async IAsyncEnumerable RunAsyncEnumerable([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var ev in events) + { + yield return ev; + } + await Task.CompletedTask; + } + } + + [TestMethod] + [Timeout(Constants.Timeout, CooperativeCancellation = true)] + public async Task RunToEndAsync_HandlesEndEvent() + { + var processor = new MockEventProcessor([End(42)]); + var exitCode = await PipeProcessorExtensions.RunToEndAsync(processor, null, null, CancellationToken); + Assert.AreEqual(42, exitCode); + } + + [TestMethod] + [Timeout(Constants.Timeout, CooperativeCancellation = true)] + public async Task RunToEndAsync_HandlesOutputCharEvent() + { + var processor = new MockEventProcessor([OutputChar('A'), End(0)]); + var pipe = new Pipe(); + + var readTask = Task.Run(async () => + { + var result = await pipe.Reader.ReadAsync(CancellationToken); + var content = Encoding.UTF8.GetString(result.Buffer.ToArray()); + Assert.AreEqual("A", content); + pipe.Reader.AdvanceTo(result.Buffer.End); + pipe.Reader.Complete(); + }, CancellationToken); + + var exitCode = await PipeProcessorExtensions.RunToEndAsync(processor, null, pipe.Writer, CancellationToken); + + await readTask; + Assert.AreEqual(0, exitCode); + } + + [TestMethod] + [Timeout(Constants.Timeout, CooperativeCancellation = true)] + public async Task RunToEndAsync_HandlesOutputIntEvent() + { + var processor = new MockEventProcessor([OutputInt(123), End(0)]); + var pipe = new Pipe(); + + var readTask = Task.Run(async () => + { + var result = await pipe.Reader.ReadAsync(CancellationToken); + var content = Encoding.UTF8.GetString(result.Buffer.ToArray()); + Assert.AreEqual("123", content); + pipe.Reader.AdvanceTo(result.Buffer.End); + pipe.Reader.Complete(); + }, CancellationToken); + + var exitCode = await PipeProcessorExtensions.RunToEndAsync(processor, null, pipe.Writer, CancellationToken); + + await readTask; + Assert.AreEqual(0, exitCode); + } + + [TestMethod] + [Timeout(Constants.Timeout, CooperativeCancellation = true)] + public async Task RunToEndAsync_HandlesInputCharEvent() + { + var capturedChar = ' '; + var processor = new MockEventProcessor([ + InputChar(c => capturedChar = c), + End(0) + ]); + + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes("X"), CancellationToken); + pipe.Writer.Complete(); + + await PipeProcessorExtensions.RunToEndAsync(processor, pipe.Reader, null, CancellationToken); + + Assert.AreEqual('X', capturedChar); + } + + [TestMethod] + [Timeout(Constants.Timeout, CooperativeCancellation = true)] + public async Task RunToEndAsync_HandlesInputIntEvent() + { + var capturedInt = 0; + var processor = new MockEventProcessor([ + InputInt(i => capturedInt = i), + End(0) + ]); + + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(BitConverter.GetBytes(123).AsMemory(), CancellationToken); + pipe.Writer.Complete(); + + await PipeProcessorExtensions.RunToEndAsync(processor, pipe.Reader, null, CancellationToken); + + Assert.AreEqual(123, capturedInt); + } + + [TestMethod] + [Timeout(Constants.Timeout, CooperativeCancellation = true)] + public void RunToEnd_HandlesEndEvent() + { + var processor = new MockEventProcessor([End(99)]); +#pragma warning disable CS0618 // 型またはメンバーが旧型式です + var exitCode = PipeProcessorExtensions.RunToEnd(processor, null, null, CancellationToken); +#pragma warning restore CS0618 // 型またはメンバーが旧型式です + Assert.AreEqual(99, exitCode); + } + + [TestMethod] + [Timeout(Constants.Timeout, CooperativeCancellation = true)] + public async Task RunToEndAsync_ThrowsArgumentNullExceptionOnNullReader() + { + var capturedChar = ' '; + var processor = new MockEventProcessor([ + InputChar(c => capturedChar = c) + ]); + await Assert.ThrowsAsync(() => PipeProcessorExtensions.RunToEndAsync(processor, null, null, CancellationToken).AsTask()); + } + + [TestMethod] + [Timeout(Constants.Timeout, CooperativeCancellation = true)] + public async Task RunToEndAsync_ThrowsArgumentNullExceptionOnNullWriter() + { + var processor = new MockEventProcessor([OutputChar('A')]); + await Assert.ThrowsAsync(() => PipeProcessorExtensions.RunToEndAsync(processor, null, null, CancellationToken).AsTask()); + } + + [TestMethod] + [Timeout(Constants.Timeout, CooperativeCancellation = true)] + public async Task RunToEndAsync_ThrowsArgumentNullExceptionOnNullReader_InputInt() + { + var capturedInt = 0; + var processor = new MockEventProcessor([ + InputInt(i => capturedInt = i) + ]); + await Assert.ThrowsAsync(() => PipeProcessorExtensions.RunToEndAsync(processor, null, null, CancellationToken).AsTask()); + } + + [TestMethod] + [Timeout(Constants.Timeout, CooperativeCancellation = true)] + public async Task RunToEndAsync_ThrowsArgumentNullExceptionOnNullWriter_OutputInt() + { + var processor = new MockEventProcessor([OutputInt(123)]); + await Assert.ThrowsAsync(() => PipeProcessorExtensions.RunToEndAsync(processor, null, null, CancellationToken).AsTask()); + } +} + +file static class Constants +{ + public const int Timeout = 2000; +} diff --git a/Processor.Extensions.IO.Tests/StringProcessorExtensionsTests.cs b/Processor.Extensions.IO.Tests/StringProcessorExtensionsTests.cs new file mode 100644 index 0000000..b2fcd80 --- /dev/null +++ b/Processor.Extensions.IO.Tests/StringProcessorExtensionsTests.cs @@ -0,0 +1,68 @@ +using Esolang.Processor; +using Esolang.Processor.Extensions.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Text; + +namespace Esolang.Processor.Tests; + +[TestClass] +public class StringProcessorExtensionsTests(TestContext TestContext) +{ + CancellationToken CancellationToken => TestContext.CancellationToken; + + class MockEventProcessor(List events) : IEventProcessor + { + public async IAsyncEnumerable RunAsyncEnumerable([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var ev in events) + { + yield return ev; + } + await Task.CompletedTask; + } + } + + [TestMethod] + [Timeout(2000, CooperativeCancellation = true)] + public async Task RunToStringAsync_ReturnsCorrectOutput() + { + var processor = new MockEventProcessor([ + IOEvent.OutputChar('H'), + IOEvent.OutputChar('i'), + IOEvent.End(0) + ]); + + var result = await StringProcessorExtensions.RunToStringAsync(processor, null, CancellationToken); + + Assert.AreEqual("Hi", result); + } + + [TestMethod] + [Timeout(2000, CooperativeCancellation = true)] + public void RunToString_ReturnsCorrectOutput() + { + var processor = new MockEventProcessor([ + IOEvent.OutputChar('A'), + IOEvent.OutputChar('B'), + IOEvent.End(0) + ]); + +#pragma warning disable CS0618 + var result = StringProcessorExtensions.RunToString(processor, null, CancellationToken); +#pragma warning restore CS0618 + Assert.AreEqual("AB", result); + } + + [TestMethod] + [Timeout(2000, CooperativeCancellation = true)] + public async Task RunToEndAsync_HandlesOutputCharEvent() + { + var processor = new MockEventProcessor([IOEvent.OutputChar('A'), IOEvent.End(0)]); + var output = new StringBuilder(); + + var exitCode = await StringProcessorExtensions.RunToEndAsync(processor, null, output, CancellationToken); + + Assert.AreEqual("A", output.ToString()); + Assert.AreEqual(0, exitCode); + } +} diff --git a/Processor.Extensions.IO.Tests/TextProcessorExtensionsTests.cs b/Processor.Extensions.IO.Tests/TextProcessorExtensionsTests.cs new file mode 100644 index 0000000..247bd26 --- /dev/null +++ b/Processor.Extensions.IO.Tests/TextProcessorExtensionsTests.cs @@ -0,0 +1,95 @@ +using Esolang.Processor; +using Esolang.Processor.Extensions.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.IO; +using System.Text; +using static Esolang.Processor.IOEvent; + +namespace Esolang.Processor.Tests; + +[TestClass] +public class TextProcessorExtensionsTests(TestContext TestContext) +{ + CancellationToken CancellationToken => TestContext.CancellationToken; + + class MockEventProcessor(List events) : IEventProcessor + { + public async IAsyncEnumerable RunAsyncEnumerable([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var ev in events) + { + yield return ev; + } + await Task.CompletedTask; + } + } + + [TestMethod] + [Timeout(2000, CooperativeCancellation = true)] + public async Task RunToEndAsync_HandlesOutputCharEvent() + { + var processor = new MockEventProcessor([OutputChar('A'), End(0)]); + using var output = new StringWriter(); + + var exitCode = await TextProcessorExtensions.RunToEndAsync(processor, null, output, CancellationToken); + + Assert.AreEqual("A", output.ToString()); + Assert.AreEqual(0, exitCode); + } + + [TestMethod] + [Timeout(2000, CooperativeCancellation = true)] + public async Task RunToEndAsync_HandlesOutputIntEvent() + { + var processor = new MockEventProcessor([OutputInt(123), End(0)]); + using var output = new StringWriter(); + + var exitCode = await TextProcessorExtensions.RunToEndAsync(processor, null, output, CancellationToken); + + Assert.AreEqual("123", output.ToString()); + Assert.AreEqual(0, exitCode); + } + + [TestMethod] + [Timeout(2000, CooperativeCancellation = true)] + public async Task RunToEndAsync_HandlesInputCharEvent() + { + var capturedChar = ' '; + var processor = new MockEventProcessor([ + InputChar(c => capturedChar = c), + End(0) + ]); + using var input = new StringReader("X"); + + await TextProcessorExtensions.RunToEndAsync(processor, input, null, CancellationToken); + + Assert.AreEqual('X', capturedChar); + } + + [TestMethod] + [Timeout(2000, CooperativeCancellation = true)] + public async Task RunToEndAsync_HandlesInputIntEvent() + { + var capturedInt = 0; + var processor = new MockEventProcessor([ + InputInt(i => capturedInt = i), + End(0) + ]); + using var input = new StringReader("123"); + + await TextProcessorExtensions.RunToEndAsync(processor, input, null, CancellationToken); + + Assert.AreEqual(123, capturedInt); + } + + [TestMethod] + [Timeout(2000, CooperativeCancellation = true)] + public void RunToEnd_HandlesEndEvent() + { + var processor = new MockEventProcessor([End(88)]); +#pragma warning disable CS0618 + var exitCode = TextProcessorExtensions.RunToEnd(processor, null, null, CancellationToken); +#pragma warning restore CS0618 + Assert.AreEqual(88, exitCode); + } +} diff --git a/Processor.Extensions.IO/Esolang.Processor.Extensions.IO.csproj b/Processor.Extensions.IO/Esolang.Processor.Extensions.IO.csproj new file mode 100644 index 0000000..ebbab3b --- /dev/null +++ b/Processor.Extensions.IO/Esolang.Processor.Extensions.IO.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0;netstandard2.1 + enable + true + Comprehensive I/O extension methods for IEventProcessor, supporting TextReader/Writer, System.IO.Pipelines, and string-based execution. + Esolang.Processor.Extensions.IO + README.md + esolang;processor;io;pipelines;text;string;extensions;abstractions + Esolang.Processor.Extensions.IO + + + + + + + + + + + + + + + + + diff --git a/Processor.Extensions.IO/PipeProcessorExtensions.cs b/Processor.Extensions.IO/PipeProcessorExtensions.cs new file mode 100644 index 0000000..8946eff --- /dev/null +++ b/Processor.Extensions.IO/PipeProcessorExtensions.cs @@ -0,0 +1,111 @@ +using System.Buffers; +using System.IO.Pipelines; +using System.Text; +using static Esolang.Processor.IOEvent; + +namespace Esolang.Processor.Extensions.IO; + +/// +/// Provides extension methods for running using and . +/// +public static class PipeProcessorExtensions +{ + /// + /// Executes the processor until it reaches an . + /// + /// The event processor. + /// The input pipe reader. + /// The output pipe writer. + /// The cancellation token. + /// The exit code. + /// Thrown when input or output is null depending on the event. + public static async ValueTask RunToEndAsync( + this IEventProcessor processor, + PipeReader? input, + PipeWriter? output, + CancellationToken cancellationToken = default) + { + await foreach (var ev in processor.RunAsyncEnumerable(cancellationToken)) + { + switch (ev) + { + case InputCharEvent inputChar: + if (input == null) + throw new ArgumentNullException(nameof(input)); + var result = await input.ReadAtLeastAsync(1, cancellationToken); + var buffer = ArrayPool.Shared.Rent(1); + try + { +#if NETSTANDARD2_1_OR_GREATER + result.Buffer.Slice(0, 1).CopyTo(buffer.AsSpan()); +#else + result.Buffer.Slice(0, 1).ToArray().CopyTo(buffer, 0); +#endif + input.AdvanceTo(result.Buffer.GetPosition(1)); + inputChar.Write((char)buffer[0]); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + break; + case InputIntEvent inputInt: + if (input == null) + throw new ArgumentNullException(nameof(input)); + var result2 = await input.ReadAtLeastAsync(1, cancellationToken); + var buffer2 = ArrayPool.Shared.Rent(1); + try + { +#if NETSTANDARD2_1_OR_GREATER + result2.Buffer.Slice(0, 4).CopyTo(buffer2.AsSpan()); +#else + result2.Buffer.Slice(0, 4).ToArray().CopyTo(buffer2, 0); +#endif + input.AdvanceTo(result2.Buffer.GetPosition(4)); + inputInt.Write(BitConverter.ToInt32(buffer2, 0)); + } + finally + { + ArrayPool.Shared.Return(buffer2); + } + break; + case OutputCharEvent outputChar: + if (output == null) + throw new ArgumentNullException(nameof(output)); + output.Write(Encoding.UTF8.GetBytes([outputChar.Output])); + await output.FlushAsync(cancellationToken); + break; + case OutputIntEvent outputInt: + if (output == null) + throw new ArgumentNullException(nameof(output)); + output.Write(Encoding.UTF8.GetBytes(outputInt.Output.ToString())); + await output.FlushAsync(cancellationToken); + break; + case EndEvent end: + return end.ExitCode; + } + } + return 0; + } + + /// + /// Executes the processor synchronously until it reaches an . + /// + /// The event processor. + /// The input pipe reader. + /// The output pipe writer. + /// The cancellation token. + /// The exit code. + [Obsolete($"Use {nameof(RunToEndAsync)} instead.")] + public static int RunToEnd( + this IEventProcessor processor, + PipeReader? input = null, + PipeWriter? output = null, + CancellationToken cancellationToken = default) + { + var result = RunToEndAsync(processor, input, output, cancellationToken); + if (result.IsCompleted) + return result.GetAwaiter().GetResult(); + return result.AsTask().GetAwaiter().GetResult(); + } +} diff --git a/Processor.Extensions.IO/README.md b/Processor.Extensions.IO/README.md new file mode 100644 index 0000000..057999d --- /dev/null +++ b/Processor.Extensions.IO/README.md @@ -0,0 +1,52 @@ +# Esolang.Processor.Extensions.IO + +Provides extension methods for running `IEventProcessor` using various I/O abstractions. + +## Installation + +```bash +dotnet add package Esolang.Processor.Extensions.IO +``` + +## Overview + +This package provides extension methods to facilitate running processors based on an event-driven I/O model. It bridges the gap between the core `IOEvent` stream and common .NET I/O types. + +## Features + +- **Text I/O**: Run processors using `TextReader` for input and `TextWriter` for output. +- **String I/O**: Convenient methods for executing with `string` input and capturing output as a `string` or `StringBuilder`. +- **Pipe I/O**: High-performance I/O using `PipeReader` and `PipeWriter` from `System.IO.Pipelines`. + +## Usage Examples + +### String I/O + +```csharp +using Esolang.Processor.Extensions.IO; + +// Run and get the output as a string +string? result = await processor.RunToStringAsync(input: "your_input"); + +// Run and write output to a StringBuilder +var sb = new StringBuilder(); +int exitCode = await processor.RunToEndAsync(input: "your_input", output: sb); +``` + +### Text I/O + +```csharp +using Esolang.Processor.Extensions.IO; + +// Run using TextReader and TextWriter +int exitCode = await processor.RunToEndAsync(Console.In, Console.Out); +``` + +### Pipe I/O + +```csharp +using Esolang.Processor.Extensions.IO; + +// Run using System.IO.Pipelines +int exitCode = await processor.RunToEndAsync(pipeReader, pipeWriter); +``` diff --git a/Processor.Extensions.IO/StringProcessorExtensions.cs b/Processor.Extensions.IO/StringProcessorExtensions.cs new file mode 100644 index 0000000..8c8bcc1 --- /dev/null +++ b/Processor.Extensions.IO/StringProcessorExtensions.cs @@ -0,0 +1,86 @@ +using System.Text; +using static Esolang.Processor.IOEvent; + +namespace Esolang.Processor.Extensions.IO; + +/// +/// Provides extension methods for running using and . +/// +public static class StringProcessorExtensions +{ + /// + /// Executes the processor until it reaches an and returns the output as a string. + /// + /// The event processor. + /// The input string. + /// The cancellation token. + /// The output string. + public static async ValueTask RunToStringAsync( + this IEventProcessor processor, + string? input = null, + CancellationToken cancellationToken = default) + { + var output = new StringBuilder(); + await processor.RunToEndAsync(input, output, cancellationToken); + return output.ToString(); + } + + /// + /// Executes the processor synchronously until it reaches an and returns the output as a string. + /// + /// The event processor. + /// The input string. + /// The cancellation token. + /// The output string. + [Obsolete($"Use {nameof(RunToStringAsync)} instead.")] + public static string? RunToString( + this IEventProcessor processor, + string? input = null, + CancellationToken cancellationToken = default) + { + var result = processor.RunToStringAsync(input, cancellationToken); + if (result.IsCompleted) + return result.GetAwaiter().GetResult(); + return result.AsTask().GetAwaiter().GetResult(); + } + + /// + /// Executes the processor until it reaches an . + /// + /// The event processor. + /// The input string. + /// The output string builder. + /// The cancellation token. + /// The exit code. + public static async ValueTask RunToEndAsync( + this IEventProcessor processor, + string? input = null, + StringBuilder? output = null, + CancellationToken cancellationToken = default) + { + using var reader = input != null ? new StringReader(input) : null; + using var writer = output != null ? new StringWriter(output) : null; + return await processor.RunToEndAsync(reader, writer, cancellationToken); + } + + /// + /// Executes the processor synchronously until it reaches an . + /// + /// The event processor. + /// The input string. + /// The output string builder. + /// The cancellation token. + /// The exit code. + [Obsolete($"Use {nameof(RunToEndAsync)} instead.")] + public static int RunToEnd( + this IEventProcessor processor, + string? input = null, + StringBuilder? output = null, + CancellationToken cancellationToken = default) + { + var result = processor.RunToEndAsync(input, output, cancellationToken); + if (result.IsCompleted) + return result.GetAwaiter().GetResult(); + return result.AsTask().GetAwaiter().GetResult(); + } +} diff --git a/Processor.Extensions.IO/TextProcessorExtensions.cs b/Processor.Extensions.IO/TextProcessorExtensions.cs new file mode 100644 index 0000000..4d61bac --- /dev/null +++ b/Processor.Extensions.IO/TextProcessorExtensions.cs @@ -0,0 +1,110 @@ +using System.Buffers; +using static Esolang.Processor.IOEvent; + +namespace Esolang.Processor.Extensions.IO; + +/// +/// Provides extension methods for running using and . +/// +public static class TextProcessorExtensions +{ + /// + /// Executes the processor until it reaches an . + /// + /// The event processor. + /// The input text reader. + /// The output text writer. + /// The cancellation token. + /// The exit code. + /// Thrown when input or output is null depending on the event. + public static async ValueTask RunToEndAsync( + this IEventProcessor processor, + TextReader? input = null, + TextWriter? output = null, + CancellationToken cancellationToken = default) + { + await foreach (var ioEvent in processor.RunAsyncEnumerable(cancellationToken)) + { + switch (ioEvent) + { + case InputCharEvent charInput: + if (input is null) + throw new ArgumentNullException(nameof(input)); + { + var buffer = ArrayPool.Shared.Rent(1); + try + { + int read; + do + { +#if NETSTANDARD2_1_OR_GREATER + read = await input.ReadAsync(buffer.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); +#else + read = await input.ReadAsync(buffer, 0, 1).ConfigureAwait(false); +#endif + if (read < 0) continue; + charInput.Write(buffer[0]); + break; + } while (read < 0 && !cancellationToken.IsCancellationRequested); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + break; + case InputIntEvent intInput: + if (input is null) + throw new ArgumentNullException(nameof(input)); + { + var inputString = await input.ReadLineAsync(); + if (int.TryParse(inputString, out var i)) + { + intInput.Write(i); + } + } + break; + case OutputCharEvent charOutput: + if (output is null) + throw new ArgumentNullException(nameof(output)); + { + await output.WriteAsync(charOutput.Output).ConfigureAwait(false); + await output.FlushAsync().ConfigureAwait(false); + } + break; + case OutputIntEvent intOutput: + if (output is null) + throw new ArgumentNullException(nameof(output)); + { + await output.WriteAsync(intOutput.Output.ToString()).ConfigureAwait(false); + await output.FlushAsync().ConfigureAwait(false); + } + break; + case EndEvent endEvent: + return endEvent.ExitCode; + } + } + return 0; + } + + /// + /// Executes the processor synchronously until it reaches an . + /// + /// The event processor. + /// The input text reader. + /// The output text writer. + /// The cancellation token. + /// The exit code. + [Obsolete($"Use {nameof(RunToEndAsync)} instead.")] + public static int RunToEnd( + this IEventProcessor processor, + TextReader? input = null, + TextWriter? output = null, + CancellationToken cancellationToken = default) + { + var result = RunToEndAsync(processor, input, output, cancellationToken); + if (result.IsCompleted) + return result.GetAwaiter().GetResult(); + return result.AsTask().GetAwaiter().GetResult(); + } +} diff --git a/README.md b/README.md index 81ae5a6..58c75f6 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,59 @@ # Esolang.Abstractions -Shared abstraction interfaces for Esolang.NET projects (Funge-98, Brainfuck, Piet). +[![.NET](https://github.com/Esolang-NET/Abstractions/actions/workflows/dotnet.yml/badge.svg)](https://github.com/Esolang-NET/Abstractions/actions/workflows/dotnet.yml) -## Overview +Unified abstractions and interfaces for the Esolang.NET ecosystem. -This repository provides common abstractions used across multiple esolang interpreter and code generator projects: +## Overview -- **Esolang.Funge** — Funge-98 parser, processor, and generator -- **Esolang.Brainfuck** — Brainfuck interpreter and generator -- **Esolang.Piet** — Piet parser, processor, and generator +This repository provides common abstractions used across multiple esolang interpreter and code generator projects such as Funge-98, Brainfuck, and Piet. It defines a unified model for execution, I/O processing, and source generation. -## Packages +## Choose Package -### Esolang.Processor.Abstractions +| Want to do | Package | +| --- | --- | +| Create code generators or binders | [Esolang.Generator.Abstractions](./Generator.Abstractions/README.md) | +| Implement a new esolang interpreter | [Esolang.Interpreter.Abstractions](./Interpreter.Abstractions/README.md) | +| Define core execution and I/O models | [Esolang.Processor.Abstractions](./Processor.Abstractions/README.md) | +| Add I/O extensions (Text, Pipelines, etc.) | [Esolang.Processor.Extensions.IO](./Processor.Extensions.IO/README.md) | -Unified processor abstractions for esolang execution. +## Install ```bash +dotnet add package Esolang.Generator.Abstractions +dotnet add package Esolang.Interpreter.Abstractions dotnet add package Esolang.Processor.Abstractions +dotnet add package Esolang.Processor.Extensions.IO ``` -Provides: +## NuGet -- **`IProcessor`** — Base interface holding a parsed program -- **`ITextProcessor`** — Execution via `TextReader`/`TextWriter` -- **`IPipeProcessor`** — Execution via `PipeReader`/`PipeWriter` +| Project | NuGet | Summary | +| --- | --- | --- | +| [Esolang.Generator.Abstractions](./Generator.Abstractions/README.md) | [![NuGet: Esolang.Generator.Abstractions](https://img.shields.io/nuget/v/Esolang.Generator.Abstractions?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/Esolang.Generator.Abstractions/) | Code generator and Roslyn binder abstractions. | +| [Esolang.Interpreter.Abstractions](./Interpreter.Abstractions/README.md) | [![NuGet: Esolang.Interpreter.Abstractions](https://img.shields.io/nuget/v/Esolang.Interpreter.Abstractions?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/Esolang.Interpreter.Abstractions/) | Base abstractions for interpreters. | +| [Esolang.Processor.Abstractions](./Processor.Abstractions/README.md) | [![NuGet: Esolang.Processor.Abstractions](https://img.shields.io/nuget/v/Esolang.Processor.Abstractions?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/Esolang.Processor.Abstractions/) | Core processor and I/O event abstractions. | +| [Esolang.Processor.Extensions.IO](./Processor.Extensions.IO/README.md) | [![NuGet: Esolang.Processor.Extensions.IO](https://img.shields.io/nuget/v/Esolang.Processor.Extensions.IO?logo=nuget&label=2.0.0)](https://www.nuget.org/packages/Esolang.Processor.Extensions.IO/) | I/O extensions for event processors. | -#### Usage +## Framework Support -```csharp -using Esolang.Processor; +| Project | Target frameworks | +| --- | --- | +| Esolang.Generator.Abstractions | netstandard2.0, netstandard2.1 | +| Esolang.Interpreter.Abstractions | net10.0 | +| Esolang.Processor.Abstractions | netstandard2.0, netstandard2.1 | +| Esolang.Processor.Extensions.IO | netstandard2.0, netstandard2.1 | -// Implement in your processor -public class MyProcessor : ITextProcessor -{ - public MyProgram Program { get; } +## Changelog - public int RunToEnd(TextReader? input = null, TextWriter? output = null, CancellationToken ct = default) - { - // Execute program and return exit code - } - - public ValueTask RunToEndAsync(TextReader? input = null, TextWriter? output = null, CancellationToken ct = default) - { - // Async variant - } -} -``` +- [CHANGELOG](./CHANGELOG.md) -## Contributing +## See also -Contributions are welcome. Please ensure code follows the project's `.editorconfig` and coding standards. +- [Esolang.Funge](https://github.com/Esolang-NET/Funge) — Funge-98 implementation +- [Esolang.Brainfuck](https://github.com/Esolang-NET/Brainfuck) — Brainfuck implementation +- [Esolang.Piet](https://github.com/Esolang-NET/Piet) — Piet implementation ## License -See [LICENSE](LICENSE) for details. +This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 0000000..9c0ab1b --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-reportgenerator-globaltool": { + "version": "5.5.10", + "commands": [ + "reportgenerator" + ], + "rollForward": false + } + } +} diff --git a/global.json b/global.json index 9d2ec9d..aaf91c4 100644 --- a/global.json +++ b/global.json @@ -2,5 +2,8 @@ "sdk": { "rollForward": "latestMinor", "version": "10.0.0" + }, + "test": { + "runner": "Microsoft.Testing.Platform" } }