From 9a1ddd850def7fd795b91eb2107b9c9c83d0f9b4 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:12:01 -0700 Subject: [PATCH 01/74] Add crossgen CI analysis agentic workflow (#9) --- .github/agents/agentic-workflows.agent.md | 102 +- .github/aw/actions-lock.json | 5 + .../workflows/crossgen2-ci-triage.lock.yml | 1226 +++++++++++++++++ .github/workflows/crossgen2-ci-triage.md | 257 ++++ 4 files changed, 1499 insertions(+), 91 deletions(-) create mode 100644 .github/workflows/crossgen2-ci-triage.lock.yml create mode 100644 .github/workflows/crossgen2-ci-triage.md diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.agent.md index b8e305fc4628df..7ed300e00cc160 100644 --- a/.github/agents/agentic-workflows.agent.md +++ b/.github/agents/agentic-workflows.agent.md @@ -30,7 +30,7 @@ Workflows may optionally include: - Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` - Workflow lock files: `.github/workflows/*.lock.yml` - Shared components: `.github/workflows/shared/*.md` -- Configuration: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/github-agentic-workflows.md +- Configuration: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/github-agentic-workflows.md ## Problems This Solves @@ -52,7 +52,7 @@ When you interact with this agent, it will: ### Create New Workflow **Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet -**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/create-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/create-agentic-workflow.md **Use cases**: - "Create a workflow that triages issues" @@ -62,7 +62,7 @@ When you interact with this agent, it will: ### Update Existing Workflow **Load when**: User wants to modify, improve, or refactor an existing workflow -**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/update-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/update-agentic-workflow.md **Use cases**: - "Add web-fetch tool to the issue-classifier workflow" @@ -72,7 +72,7 @@ When you interact with this agent, it will: ### Debug Workflow **Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors -**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/debug-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/debug-agentic-workflow.md **Use cases**: - "Why is this workflow failing?" @@ -82,7 +82,7 @@ When you interact with this agent, it will: ### Upgrade Agentic Workflows **Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations -**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/upgrade-agentic-workflows.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/upgrade-agentic-workflows.md **Use cases**: - "Upgrade all workflows to the latest version" @@ -92,7 +92,7 @@ When you interact with this agent, it will: ### Create a Report-Generating Workflow **Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment -**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/report.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/report.md **Use cases**: - "Create a weekly CI health report" @@ -102,7 +102,7 @@ When you interact with this agent, it will: ### Create Shared Agentic Workflow **Load when**: User wants to create a reusable workflow component or wrap an MCP server -**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/create-shared-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/create-shared-agentic-workflow.md **Use cases**: - "Create a shared component for Notion integration" @@ -112,7 +112,7 @@ When you interact with this agent, it will: ### Fix Dependabot PRs **Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`) -**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/dependabot.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/dependabot.md **Use cases**: - "Fix the open Dependabot PRs for npm dependencies" @@ -122,7 +122,7 @@ When you interact with this agent, it will: ### Analyze Test Coverage **Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy. -**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/test-coverage.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/test-coverage.md **Use cases**: - "Create a workflow that comments coverage on PRs" @@ -169,90 +169,10 @@ gh aw compile --validate ## Important Notes -- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/github-agentic-workflows.md for complete documentation +- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/github-agentic-workflows.md for complete documentation - Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud - Workflows must be compiled to `.lock.yml` files before running in GitHub Actions - **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF - Follow security best practices: minimal permissions, explicit network access, no template injection -- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/network.md for the full list of valid ecosystem identifiers and domain patterns. +- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/network.md for the full list of valid ecosystem identifiers and domain patterns. - **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself. - -## Repository-Specific Requirements: Copilot PAT Pool - -**⚠️ MANDATORY**: Every agentic workflow in this repository **must** include the `select-copilot-pat` integration. This selects a random Copilot PAT from a numbered pool of secrets to avoid rate-limiting from a single shared PAT. This is a stop-gap workaround until organization-level billing is offered for agentic workflows. - -These elements are **not supported through imports**, so they must be copied into the frontmatter of every agentic workflow. See `.github/actions/select-copilot-pat/README.md` for full documentation and `.github/workflows/copilot-echo.md` for a working example. - -When **creating** or **updating** an agentic workflow, always include the following three frontmatter blocks. These go between the `---` markers alongside the workflow's other frontmatter (permissions, safe-outputs, etc.). - -### Comment block (placed before the steps, inside the `on:` block) - -Always include this comment block before the steps to explain the PAT pool override: - -```yaml -# ############################################################### -# Override the COPILOT_GITHUB_TOKEN secret usage for the workflow -# with a randomly-selected token from a pool of secrets. -# -# As soon as organization-level billing is offered for Agentic -# Workflows, this stop-gap approach will be removed. -# -# See: /.github/actions/select-copilot-pat/README.md -# ############################################################### -``` - -### Block 1: Pre-activation steps (nested under `on:`) - -Add this under the `on:` key, at the same level as the trigger configuration, immediately after the comment block above: - -```yaml - # Add the pre-activation step of selecting a random PAT from the supplied secrets - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_PAT_0 }} - SECRET_1: ${{ secrets.COPILOT_PAT_1 }} - SECRET_2: ${{ secrets.COPILOT_PAT_2 }} - SECRET_3: ${{ secrets.COPILOT_PAT_3 }} - SECRET_4: ${{ secrets.COPILOT_PAT_4 }} - SECRET_5: ${{ secrets.COPILOT_PAT_5 }} - SECRET_6: ${{ secrets.COPILOT_PAT_6 }} - SECRET_7: ${{ secrets.COPILOT_PAT_7 }} - SECRET_8: ${{ secrets.COPILOT_PAT_8 }} - SECRET_9: ${{ secrets.COPILOT_PAT_9 }} -``` - -### Block 2: Pre-activation job outputs (top-level `jobs:`) - -```yaml -jobs: - pre-activation: - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} -``` - -### Block 3: Engine configuration with PAT override (top-level `engine:`) - -```yaml -engine: - id: copilot - env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_PAT_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} -``` - -**Important notes about the engine block:** -- The `COPILOT_GITHUB_TOKEN` `case()` expression **must** remain on a single line — line breaks cause syntax errors in the compiled workflow. -- If no `COPILOT_PAT_#` secrets are configured, the expression falls back to the default `COPILOT_GITHUB_TOKEN` secret. -- Do **not** specify `engine: copilot` as a simple string — use the object form shown above so the `env:` override can be included. diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 959efc4f8604ed..c19b6911647a53 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -14,6 +14,11 @@ "repo": "github/gh-aw-actions/setup", "version": "v0.63.1", "sha": "53e09ec0be6271e81a69f51ef93f37212c8834b0" + }, + "github/gh-aw-actions/setup@v0.64.2": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.64.2", + "sha": "f22886a9607f5c27e79742a8bfc5faa34737138b" } } } diff --git a/.github/workflows/crossgen2-ci-triage.lock.yml b/.github/workflows/crossgen2-ci-triage.lock.yml new file mode 100644 index 00000000000000..7bf3bd0c517761 --- /dev/null +++ b/.github/workflows/crossgen2-ci-triage.lock.yml @@ -0,0 +1,1226 @@ +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.64.2). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Daily triage of crossgen2 CI pipeline failures - analyzes builds, creates issues, and assigns Copilot to fix or disable failing tests +# +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"bc7ab7d15a655712927f8a98df48ffb24d1bfd5c771a0afe5013ab470dd750b1","compiler_version":"v0.64.2","strict":true,"agent_id":"copilot"} + +name: "Crossgen2 CI Failure Triage" +"on": + schedule: + - cron: "51 12 * * 1-5" + # Friendly format: daily on weekdays (scattered) + # steps: # Steps injected into pre-activation job + # - name: Checkout the select-copilot-pat action folder + # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + # with: + # fetch-depth: 1 + # persist-credentials: false + # sparse-checkout: .github/actions/select-copilot-pat + # sparse-checkout-cone-mode: true + # - env: + # SECRET_0: ${{ secrets.COPILOT_PAT_0 }} + # SECRET_1: ${{ secrets.COPILOT_PAT_1 }} + # SECRET_2: ${{ secrets.COPILOT_PAT_2 }} + # SECRET_3: ${{ secrets.COPILOT_PAT_3 }} + # SECRET_4: ${{ secrets.COPILOT_PAT_4 }} + # SECRET_5: ${{ secrets.COPILOT_PAT_5 }} + # SECRET_6: ${{ secrets.COPILOT_PAT_6 }} + # SECRET_7: ${{ secrets.COPILOT_PAT_7 }} + # SECRET_8: ${{ secrets.COPILOT_PAT_8 }} + # SECRET_9: ${{ secrets.COPILOT_PAT_9 }} + # id: select-copilot-pat + # name: Select Copilot token from pool + # uses: ./.github/actions/select-copilot-pat + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Crossgen2 CI Failure Triage" + +jobs: + activation: + needs: pre_activation + if: needs.pre_activation.outputs.activated == 'true' + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "latest" + GH_AW_INFO_AGENT_VERSION: "latest" + GH_AW_INFO_CLI_VERSION: "v0.64.2" + GH_AW_INFO_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dev.azure.com","helix.dot.net","mihubot.xyz"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.1" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: ${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "crossgen2-ci-triage.lock.yml" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh + { + cat << 'GH_AW_PROMPT_9da1e209676f482d_EOF' + + GH_AW_PROMPT_9da1e209676f482d_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_9da1e209676f482d_EOF' + + Tools: create_issue(max:10), missing_tool, missing_data, noop + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_9da1e209676f482d_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_9da1e209676f482d_EOF' + + GH_AW_PROMPT_9da1e209676f482d_EOF + cat << 'GH_AW_PROMPT_9da1e209676f482d_EOF' + {{#runtime-import .github/workflows/crossgen2-ci-triage.md}} + GH_AW_PROMPT_9da1e209676f482d_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ALLOWED_EXTENSIONS: '' + GH_AW_CACHE_DESCRIPTION: '' + GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/' + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_ALLOWED_EXTENSIONS: process.env.GH_AW_ALLOWED_EXTENSIONS, + GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION, + GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash ${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash ${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + issues: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: crossgen2citriage + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Set runtime paths + id: set-runtime-paths + run: | + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash ${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure gh CLI for GitHub Enterprise + run: bash ${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh + env: + GH_TOKEN: ${{ github.token }} + # Cache memory file share configuration from frontmatter processed below + - name: Create cache-memory directory + run: bash ${RUNNER_TEMP}/gh-aw/actions/create_cache_memory_dir.sh + - name: Restore cache-memory file share data + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + key: memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + restore-keys: | + memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.1 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.1 ghcr.io/github/gh-aw-firewall/squid:0.25.1 ghcr.io/github/gh-aw-mcpg:v0.2.6 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_ff440e9106c31114_EOF' + {"create_issue":{"assignees":["copilot"],"expires":720,"labels":["area-CodeGen-coreclr"],"max":10,"title_prefix":"[Crossgen2 CI] "},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"}} + GH_AW_SAFE_OUTPUTS_CONFIG_ff440e9106c31114_EOF + - name: Write Safe Outputs Tools + run: | + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_4176a0d43c6caf3b_EOF' + { + "description_suffixes": { + "create_issue": " CONSTRAINTS: Maximum 10 issue(s) can be created. Title will be prefixed with \"[Crossgen2 CI] \". Labels [\"area-CodeGen-coreclr\"] will be automatically added. Assignees [\"copilot\"] will be automatically assigned." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_SAFE_OUTPUTS_TOOLS_META_4176a0d43c6caf3b_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_43914b81eb4dcd25_EOF' + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + } + } + GH_AW_SAFE_OUTPUTS_VALIDATION_43914b81eb4dcd25_EOF + node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash ${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.6' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_0749bad54f9020cf_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests,actions,search" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_0749bad54f9020cf_EOF + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 30 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dev.azure.com,github.com,helix.dot.net,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,mihubot.xyz,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.1 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.64.2 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash ${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash ${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dev.azure.com,github.com,helix.dot.net,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,mihubot.xyz,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_GITHUB_REFS: "" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Upload cache-memory data as artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + if: always() + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + if-no-files-found: ignore + - name: Upload firewall audit logs + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: firewall-audit-logs + path: | + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + - update_cache_memory + if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + concurrency: + group: "gh-aw-conclusion-crossgen2-ci-triage" + cancel-in-progress: false + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" + GH_AW_TRACKER_ID: "crossgen2-ci-triage" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" + GH_AW_TRACKER_ID: "crossgen2-ci-triage" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" + GH_AW_TRACKER_ID: "crossgen2-ci-triage" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "crossgen2-ci-triage" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_ASSIGN_COPILOT_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.assign_copilot_failure_count }} + GH_AW_ASSIGN_COPILOT_ERRORS: ${{ needs.safe_outputs.outputs.assign_copilot_errors }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_TIMEOUT_MINUTES: "30" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" + GH_AW_TRACKER_ID: "crossgen2-ci-triage" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + + detection: + needs: agent + if: always() && needs.agent.result != 'skipped' + runs-on: ubuntu-latest + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + # --- Threat Detection --- + - name: Download container images + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.1 ghcr.io/github/gh-aw-firewall/squid:0.25.1 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + WORKFLOW_NAME: "Crossgen2 CI Failure Triage" + WORKFLOW_DESCRIPTION: "Daily triage of crossgen2 CI pipeline failures - analyzes builds, creates issues, and assigns Copilot to fix or disable failing tests" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Install GitHub Copilot CLI + run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.1 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.1 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.64.2 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + + pre_activation: + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} + matched_command: '' + select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + await main(); + - name: Checkout the select-copilot-pat action folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + sparse-checkout: .github/actions/select-copilot-pat + sparse-checkout-cone-mode: true + - name: Select Copilot token from pool + id: select-copilot-pat + uses: ./.github/actions/select-copilot-pat + env: + SECRET_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_9: ${{ secrets.COPILOT_PAT_9 }} + + safe_outputs: + needs: + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/crossgen2-ci-triage" + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_TRACKER_ID: "crossgen2-ci-triage" + GH_AW_WORKFLOW_ID: "crossgen2-ci-triage" + GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" + outputs: + assign_copilot_errors: ${{ steps.assign_copilot_to_created_issues.outputs.assign_copilot_errors }} + assign_copilot_failure_count: ${{ steps.assign_copilot_to_created_issues.outputs.assign_copilot_failure_count }} + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dev.azure.com,github.com,helix.dot.net,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,mihubot.xyz,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"assignees\":[\"copilot\"],\"expires\":720,\"labels\":[\"area-CodeGen-coreclr\"],\"max\":10,\"title_prefix\":\"[Crossgen2 CI] \"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"}}" + GH_AW_ASSIGN_COPILOT: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Assign Copilot to created issues + id: assign_copilot_to_created_issues + if: steps.process_safe_outputs.outputs.issues_to_assign_copilot != '' + continue-on-error: true + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_ISSUES_TO_ASSIGN_COPILOT: ${{ steps.process_safe_outputs.outputs.issues_to_assign_copilot }} + with: + github-token: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/assign_copilot_to_created_issues.cjs'); + await main(); + - name: Upload Safe Output Items + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-output-items + path: /tmp/gh-aw/safe-output-items.jsonl + if-no-files-found: ignore + + update_cache_memory: + needs: + - agent + - detection + if: always() && needs.detection.result == 'success' + runs-on: ubuntu-latest + permissions: {} + env: + GH_AW_WORKFLOW_ID_SANITIZED: crossgen2citriage + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download cache-memory artifact (default) + id: download_cache_default + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Check if cache-memory folder has content (default) + id: check_cache_default + shell: bash + run: | + if [ -d "/tmp/gh-aw/cache-memory" ] && [ "$(ls -A /tmp/gh-aw/cache-memory 2>/dev/null)" ]; then + echo "has_content=true" >> "$GITHUB_OUTPUT" + else + echo "has_content=false" >> "$GITHUB_OUTPUT" + fi + - name: Save cache-memory to cache (default) + if: steps.check_cache_default.outputs.has_content == 'true' + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + key: memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + diff --git a/.github/workflows/crossgen2-ci-triage.md b/.github/workflows/crossgen2-ci-triage.md new file mode 100644 index 00000000000000..590b432c663311 --- /dev/null +++ b/.github/workflows/crossgen2-ci-triage.md @@ -0,0 +1,257 @@ +--- +description: "Daily triage of crossgen2 CI pipeline failures - analyzes builds, creates issues, and assigns Copilot to fix or disable failing tests" + +on: + schedule: daily on weekdays + workflow_dispatch: + + # ############################################################### + # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow + # with a randomly-selected token from a pool of secrets. + # + # As soon as organization-level billing is offered for Agentic + # Workflows, this stop-gap approach will be removed. + # + # See: /.github/actions/select-copilot-pat/README.md + # ############################################################### + + # Add the pre-activation step of selecting a random PAT from the supplied secrets + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + name: Checkout the select-copilot-pat action folder + with: + persist-credentials: false + sparse-checkout: .github/actions/select-copilot-pat + sparse-checkout-cone-mode: true + fetch-depth: 1 + + - id: select-copilot-pat + name: Select Copilot token from pool + uses: ./.github/actions/select-copilot-pat + env: + SECRET_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_9: ${{ secrets.COPILOT_PAT_9 }} + +# Add the pre-activation output of the randomly selected PAT +jobs: + pre-activation: + outputs: + copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} + +# Override the COPILOT_GITHUB_TOKEN expression used in the activation job +# Consume the PAT number from the pre-activation step and select the corresponding secret +engine: + id: copilot + env: + # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow + # If none of the `COPILOT_PAT_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} + +timeout-minutes: 30 + +permissions: + contents: read + issues: read + actions: read + +tools: + github: + toolsets: [default, actions, search] + web-fetch: + cache-memory: true + +network: + allowed: + - defaults + - dev.azure.com + - helix.dot.net + - mihubot.xyz + +safe-outputs: + mentions: false + allowed-github-references: [] + create-issue: + max: 10 + assignees: [copilot] + labels: [area-CodeGen-coreclr] + title-prefix: "[Crossgen2 CI] " + expires: 30 + noop: + +tracker-id: crossgen2-ci-triage +--- + +# Crossgen2 CI Failure Triage + +You are an automated CI triage agent for the dotnet/runtime repository. Your job is to analyze recent failures in crossgen2-related CI pipelines, identify new unknown test failures, and create actionable GitHub issues assigned to Copilot Coding Agent. + +## Target Pipelines + +Analyze failures from these Azure DevOps pipelines (org: `dnceng-public`, project: `public`): + +1. `runtime-coreclr crossgen2` +2. `runtime-coreclr crossgen2-composite` +3. `runtime-coreclr crossgen2 outerloop` +4. `runtime-coreclr crossgen2-composite gcstress` + +## Step 1: Discover Failed Builds + +Query Azure DevOps for builds completed in the last 48 hours (to cover weekends on Monday) that have failures. + +For each target pipeline: + +1. **Look up the pipeline definition ID**: + ``` + curl -s "https://dev.azure.com/dnceng-public/public/_apis/build/definitions?name=&api-version=7.0" + ``` + Extract the `id` field from the response. + +2. **Query failed builds** using the definition ID: + ``` + curl -s "https://dev.azure.com/dnceng-public/public/_apis/build/builds?definitions=&minTime=&resultFilter=failed&statusFilter=completed&branchName=refs/heads/main&api-version=7.0" + ``` + Use the current UTC time minus 48 hours for `minTime` in ISO 8601 format. + +3. **Collect build IDs** for all failed builds across all four pipelines. + +If no failed builds are found across any pipeline, call the `noop` safe output with a message explaining that no crossgen2 pipeline failures were found in the last 48 hours. + +## Step 2: Analyze Each Failed Build + +For each failed build, use the CI Analysis skill script: + +```bash +pwsh .github/skills/ci-analysis/scripts/Get-CIStatus.ps1 -BuildId -ShowLogs -SearchMihuBot -ContinueOnError +``` + +From the output, extract: +- **Failed job names** and their error categories +- **Failed test names** and error messages +- **Helix work item details** (test names, error snippets, console logs) +- **Known issue matches** from Build Analysis +- **The `[CI_ANALYSIS_SUMMARY]` JSON block** for structured analysis + +### Filtering Known Issues + +Skip failures that are already matched to known issues by Build Analysis. Focus only on **unknown/untracked failures** — these are the ones that need new issues. + +### Check Cache Memory + +Read from `cache-memory` a file named `triaged-builds.json` (if it exists). This contains build IDs and failure signatures that have already been triaged. Skip any failures that match entries in this file. + +## Step 3: Search for Existing Issues + +For each unknown failure, search GitHub for existing issues that might already track it: + +1. **Search by test name**: Use GitHub search to find open issues mentioning the failing test name in `dotnet/runtime`: + - Search with the test class name and method name + - Check issues with labels `area-CodeGen-coreclr` or `Known Build Error` + +2. **Search by error signature**: If the test name search yields no results, search for distinctive parts of the error message. + +3. **Check MihuBot results**: The CI analysis script with `-SearchMihuBot` may have already found related issues — use those results. + +If an existing open issue already tracks the failure, skip creating a new one. Note the existing issue number in your analysis. + +## Step 4: Create Issues for New Failures + +For each genuinely new, untracked failure, create a GitHub issue using the `create-issue` safe output. + +### Assess Fix Complexity + +Before creating the issue, assess whether the failure looks **simply solvable**: + +**Simply solvable** (instruct Copilot to fix the root cause): +- An assertion message clearly indicates what value was expected vs actual +- A null reference exception with an obvious missing null check +- A simple type mismatch or casting error +- A race condition with an obvious synchronization fix +- The error message directly points to the fix + +**Not simply solvable** (instruct Copilot to disable the test): +- Complex logic failures requiring deep domain knowledge +- Intermittent/flaky failures without clear reproduction pattern +- Failures related to infrastructure or environment issues +- Crashes or timeouts without clear root cause +- Failures that require understanding complex crossgen2 internals + +### Issue Format + +Create issues with the following structure: + +**Title**: A concise description of the failing test (the `[Crossgen2 CI]` prefix is added automatically) + +**Body**: + +```markdown +### Failure Details + +- **Pipeline**: +- **Build**: [](https://dev.azure.com/dnceng-public/public/_build/results?buildId=) +- **Test**: `` +- **Configuration**: +- **Error Category**: + +### Error Output + +
+Error details + +\`\`\` + +\`\`\` + +
+ +### Helix Details + +- **Job**: +- **Work Item**: +- **Console Log**: + +### Recommended Action + + + +**Option A (simple fix):** +The failure appears to be straightforward to fix. Please investigate and fix the root cause: +- +- + +**Option B (disable test):** +This failure requires deeper investigation. Please disable the failing test by adding an `[ActiveIssue]` attribute referencing this issue: +- Locate the test method or test class +- Add `[ActiveIssue("https://github.com/dotnet/runtime/issues/ISSUE_NUMBER")]` attribute +- If the test is in a `.csproj` with crossgen2-specific conditions, the disable may need to target specific configurations +``` + +For Option B (disabling tests), provide specific guidance: +- If you can identify the test source file path, mention it +- Suggest the correct `[ActiveIssue]` attribute syntax +- Note which configurations to disable for (e.g., only crossgen2, only specific OS) + +## Step 5: Update Cache Memory + +After processing all builds, write the updated `triaged-builds.json` to `cache-memory` with: +- Build IDs that were analyzed +- Failure signatures (test name + error category) that were triaged +- Timestamp of this triage run + +Use filesystem-safe timestamp format `YYYY-MM-DD-HH-MM-SS` (no colons). + +## Important Guidelines + +- **Do not create duplicate issues.** Always search thoroughly before creating. +- **Do not create issues for known/tracked failures.** If Build Analysis has already matched a failure to a known issue, skip it. +- **Be conservative with "simple fix" assessments.** When in doubt, instruct Copilot to disable the test rather than attempt a fix. +- **Include enough context in issues** for Copilot Coding Agent to act without further investigation. +- **Group related failures.** If the same test fails across multiple pipelines or configurations, create a single issue covering all occurrences. +- If there are no new unknown failures to report, call the `noop` safe output explaining what you analyzed and that all failures are either known or already tracked. From 87296e1e94b1a3a7537408a83d986544990caf6f Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:44:48 -0700 Subject: [PATCH 02/74] Crossgen2 ci triage workflow (#10) * Add crossgen CI analysis agentic workflow * Use Opus --- .../workflows/crossgen2-ci-triage.lock.yml | 42 +++++++++---------- .github/workflows/crossgen2-ci-triage.md | 1 + 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/.github/workflows/crossgen2-ci-triage.lock.yml b/.github/workflows/crossgen2-ci-triage.lock.yml index 7bf3bd0c517761..86527134d96882 100644 --- a/.github/workflows/crossgen2-ci-triage.lock.yml +++ b/.github/workflows/crossgen2-ci-triage.lock.yml @@ -22,7 +22,7 @@ # # Daily triage of crossgen2 CI pipeline failures - analyzes builds, creates issues, and assigns Copilot to fix or disable failing tests # -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"bc7ab7d15a655712927f8a98df48ffb24d1bfd5c771a0afe5013ab470dd750b1","compiler_version":"v0.64.2","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"d5063c47392525950416b9d1f3a7c25be9e52de2e0fa7d3555921fa969d07b5b","compiler_version":"v0.64.2","strict":true,"agent_id":"copilot","agent_model":"claude-opus-4.6"} name: "Crossgen2 CI Failure Triage" "on": @@ -89,7 +89,7 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }} + GH_AW_INFO_MODEL: "claude-opus-4.6" GH_AW_INFO_VERSION: "latest" GH_AW_INFO_AGENT_VERSION: "latest" GH_AW_INFO_CLI_VERSION: "v0.64.2" @@ -150,15 +150,15 @@ jobs: run: | bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh { - cat << 'GH_AW_PROMPT_9da1e209676f482d_EOF' + cat << 'GH_AW_PROMPT_8b5caa7cf197bc86_EOF' - GH_AW_PROMPT_9da1e209676f482d_EOF + GH_AW_PROMPT_8b5caa7cf197bc86_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_9da1e209676f482d_EOF' + cat << 'GH_AW_PROMPT_8b5caa7cf197bc86_EOF' Tools: create_issue(max:10), missing_tool, missing_data, noop @@ -190,14 +190,14 @@ jobs: {{/if}} - GH_AW_PROMPT_9da1e209676f482d_EOF + GH_AW_PROMPT_8b5caa7cf197bc86_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_9da1e209676f482d_EOF' + cat << 'GH_AW_PROMPT_8b5caa7cf197bc86_EOF' - GH_AW_PROMPT_9da1e209676f482d_EOF - cat << 'GH_AW_PROMPT_9da1e209676f482d_EOF' + GH_AW_PROMPT_8b5caa7cf197bc86_EOF + cat << 'GH_AW_PROMPT_8b5caa7cf197bc86_EOF' {{#runtime-import .github/workflows/crossgen2-ci-triage.md}} - GH_AW_PROMPT_9da1e209676f482d_EOF + GH_AW_PROMPT_8b5caa7cf197bc86_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -373,12 +373,12 @@ jobs: mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_ff440e9106c31114_EOF' + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_00522bd93e262eb1_EOF' {"create_issue":{"assignees":["copilot"],"expires":720,"labels":["area-CodeGen-coreclr"],"max":10,"title_prefix":"[Crossgen2 CI] "},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"}} - GH_AW_SAFE_OUTPUTS_CONFIG_ff440e9106c31114_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_00522bd93e262eb1_EOF - name: Write Safe Outputs Tools run: | - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_4176a0d43c6caf3b_EOF' + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_04f171aca78a4b9b_EOF' { "description_suffixes": { "create_issue": " CONSTRAINTS: Maximum 10 issue(s) can be created. Title will be prefixed with \"[Crossgen2 CI] \". Labels [\"area-CodeGen-coreclr\"] will be automatically added. Assignees [\"copilot\"] will be automatically assigned." @@ -386,8 +386,8 @@ jobs: "repo_params": {}, "dynamic_tools": [] } - GH_AW_SAFE_OUTPUTS_TOOLS_META_4176a0d43c6caf3b_EOF - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_43914b81eb4dcd25_EOF' + GH_AW_SAFE_OUTPUTS_TOOLS_META_04f171aca78a4b9b_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_3bfef588d8637126_EOF' { "create_issue": { "defaultMax": 1, @@ -480,7 +480,7 @@ jobs: } } } - GH_AW_SAFE_OUTPUTS_VALIDATION_43914b81eb4dcd25_EOF + GH_AW_SAFE_OUTPUTS_VALIDATION_3bfef588d8637126_EOF node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config @@ -548,7 +548,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.6' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_0749bad54f9020cf_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_6cb382831404933d_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "github": { @@ -589,7 +589,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_0749bad54f9020cf_EOF + GH_AW_MCP_CONFIG_6cb382831404933d_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -611,7 +611,7 @@ jobs: env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + COPILOT_MODEL: claude-opus-4.6 GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1010,7 +1010,7 @@ jobs: env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + COPILOT_MODEL: claude-opus-4.6 GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_VERSION: v0.64.2 @@ -1105,7 +1105,7 @@ jobs: env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/crossgen2-ci-triage" GH_AW_ENGINE_ID: "copilot" - GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_MODEL: "claude-opus-4.6" GH_AW_TRACKER_ID: "crossgen2-ci-triage" GH_AW_WORKFLOW_ID: "crossgen2-ci-triage" GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" diff --git a/.github/workflows/crossgen2-ci-triage.md b/.github/workflows/crossgen2-ci-triage.md index 590b432c663311..f7d2b07273df24 100644 --- a/.github/workflows/crossgen2-ci-triage.md +++ b/.github/workflows/crossgen2-ci-triage.md @@ -50,6 +50,7 @@ jobs: # Consume the PAT number from the pre-activation step and select the corresponding secret engine: id: copilot + model: claude-opus-4.6 env: # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow # If none of the `COPILOT_PAT_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used From 130a3c0a7a0cf8f5406ae095889c2e9b7478c1e2 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:00:39 -0700 Subject: [PATCH 03/74] Crossgen2 ci triage workflow (#19) * Add crossgen CI analysis agentic workflow * Use Opus * Fix DIFC integrity filtering and improve issue quality in crossgen2 CI triage - Add min-integrity: none to tools.github so the agent can read all dotnet/runtime issues regardless of author association (fixes false positive 'new' issues when existing issues were invisible) - Require specific fully qualified test names and verbatim error output in created issues instead of summaries - Add pull-requests: read permission to fix compilation warning - Recompile lock.yml Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/aw/actions-lock.json | 20 +++++++ .../workflows/crossgen2-ci-triage.lock.yml | 59 +++++++++---------- .github/workflows/crossgen2-ci-triage.md | 37 ++++++++++-- 3 files changed, 81 insertions(+), 35 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index c19b6911647a53..1cd3695af0529a 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,10 +1,30 @@ { "entries": { + "actions/cache/restore@v5.0.4": { + "repo": "actions/cache/restore", + "version": "v5.0.4", + "sha": "668228422ae6a00e4ad889ee87cd7109ec5666a7" + }, + "actions/cache/save@v5.0.4": { + "repo": "actions/cache/save", + "version": "v5.0.4", + "sha": "668228422ae6a00e4ad889ee87cd7109ec5666a7" + }, + "actions/download-artifact@v8.0.1": { + "repo": "actions/download-artifact", + "version": "v8.0.1", + "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" + }, "actions/github-script@v8": { "repo": "actions/github-script", "version": "v8", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" }, + "actions/upload-artifact@v7": { + "repo": "actions/upload-artifact", + "version": "v7", + "sha": "bbbca2ddaa5d8feaa63e36b76fdaad77386f024f" + }, "github/gh-aw-actions/setup@v0.63.0": { "repo": "github/gh-aw-actions/setup", "version": "v0.63.0", diff --git a/.github/workflows/crossgen2-ci-triage.lock.yml b/.github/workflows/crossgen2-ci-triage.lock.yml index 86527134d96882..db03d9c5f946f0 100644 --- a/.github/workflows/crossgen2-ci-triage.lock.yml +++ b/.github/workflows/crossgen2-ci-triage.lock.yml @@ -22,7 +22,7 @@ # # Daily triage of crossgen2 CI pipeline failures - analyzes builds, creates issues, and assigns Copilot to fix or disable failing tests # -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"d5063c47392525950416b9d1f3a7c25be9e52de2e0fa7d3555921fa969d07b5b","compiler_version":"v0.64.2","strict":true,"agent_id":"copilot","agent_model":"claude-opus-4.6"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"d44a5e48fa6b2fc3f700bf3858f0bf05e8b9ca09bbdc310eb664b57cfc672c5e","compiler_version":"v0.64.2","strict":true,"agent_id":"copilot","agent_model":"claude-opus-4.6"} name: "Crossgen2 CI Failure Triage" "on": @@ -150,15 +150,15 @@ jobs: run: | bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh { - cat << 'GH_AW_PROMPT_8b5caa7cf197bc86_EOF' + cat << 'GH_AW_PROMPT_cbb12d0a2eefb802_EOF' - GH_AW_PROMPT_8b5caa7cf197bc86_EOF + GH_AW_PROMPT_cbb12d0a2eefb802_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_8b5caa7cf197bc86_EOF' + cat << 'GH_AW_PROMPT_cbb12d0a2eefb802_EOF' Tools: create_issue(max:10), missing_tool, missing_data, noop @@ -190,14 +190,14 @@ jobs: {{/if}} - GH_AW_PROMPT_8b5caa7cf197bc86_EOF + GH_AW_PROMPT_cbb12d0a2eefb802_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_8b5caa7cf197bc86_EOF' + cat << 'GH_AW_PROMPT_cbb12d0a2eefb802_EOF' - GH_AW_PROMPT_8b5caa7cf197bc86_EOF - cat << 'GH_AW_PROMPT_8b5caa7cf197bc86_EOF' + GH_AW_PROMPT_cbb12d0a2eefb802_EOF + cat << 'GH_AW_PROMPT_cbb12d0a2eefb802_EOF' {{#runtime-import .github/workflows/crossgen2-ci-triage.md}} - GH_AW_PROMPT_8b5caa7cf197bc86_EOF + GH_AW_PROMPT_cbb12d0a2eefb802_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -277,6 +277,7 @@ jobs: actions: read contents: read issues: read + pull-requests: read concurrency: group: "gh-aw-copilot-${{ github.workflow }}" env: @@ -356,16 +357,12 @@ jobs: GH_HOST: github.com - name: Install AWF binary run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.1 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + - name: Parse integrity filter lists + id: parse-guard-vars env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash ${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh - name: Download container images run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.1 ghcr.io/github/gh-aw-firewall/squid:0.25.1 ghcr.io/github/gh-aw-mcpg:v0.2.6 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config @@ -373,12 +370,12 @@ jobs: mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_00522bd93e262eb1_EOF' + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_4a4a87ef2c599b25_EOF' {"create_issue":{"assignees":["copilot"],"expires":720,"labels":["area-CodeGen-coreclr"],"max":10,"title_prefix":"[Crossgen2 CI] "},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"}} - GH_AW_SAFE_OUTPUTS_CONFIG_00522bd93e262eb1_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_4a4a87ef2c599b25_EOF - name: Write Safe Outputs Tools run: | - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_04f171aca78a4b9b_EOF' + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_33af0b75731912b2_EOF' { "description_suffixes": { "create_issue": " CONSTRAINTS: Maximum 10 issue(s) can be created. Title will be prefixed with \"[Crossgen2 CI] \". Labels [\"area-CodeGen-coreclr\"] will be automatically added. Assignees [\"copilot\"] will be automatically assigned." @@ -386,8 +383,8 @@ jobs: "repo_params": {}, "dynamic_tools": [] } - GH_AW_SAFE_OUTPUTS_TOOLS_META_04f171aca78a4b9b_EOF - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_3bfef588d8637126_EOF' + GH_AW_SAFE_OUTPUTS_TOOLS_META_33af0b75731912b2_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_e5513dbdb514de38_EOF' { "create_issue": { "defaultMax": 1, @@ -480,7 +477,7 @@ jobs: } } } - GH_AW_SAFE_OUTPUTS_VALIDATION_3bfef588d8637126_EOF + GH_AW_SAFE_OUTPUTS_VALIDATION_e5513dbdb514de38_EOF node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config @@ -526,8 +523,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail @@ -548,7 +543,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.6' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_6cb382831404933d_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_4fe7628130205168_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "github": { @@ -562,8 +557,10 @@ jobs: }, "guard-policies": { "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all" } } }, @@ -589,7 +586,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_6cb382831404933d_EOF + GH_AW_MCP_CONFIG_4fe7628130205168_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -782,6 +779,8 @@ jobs: /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ /tmp/gh-aw/safeoutputs.jsonl diff --git a/.github/workflows/crossgen2-ci-triage.md b/.github/workflows/crossgen2-ci-triage.md index f7d2b07273df24..203153c7f2ba22 100644 --- a/.github/workflows/crossgen2-ci-triage.md +++ b/.github/workflows/crossgen2-ci-triage.md @@ -62,10 +62,12 @@ permissions: contents: read issues: read actions: read + pull-requests: read tools: github: toolsets: [default, actions, search] + min-integrity: none web-fetch: cache-memory: true @@ -133,13 +135,16 @@ For each failed build, use the CI Analysis skill script: pwsh .github/skills/ci-analysis/scripts/Get-CIStatus.ps1 -BuildId -ShowLogs -SearchMihuBot -ContinueOnError ``` -From the output, extract: +From the output, extract and preserve: - **Failed job names** and their error categories -- **Failed test names** and error messages -- **Helix work item details** (test names, error snippets, console logs) +- **Specific test names**: Fully qualified test class and method names (e.g., `System.Net.Security.Tests.SslStreamTest.ConnectAsync_InvalidCertificate_Throws`) +- **Error messages and stack traces**: Copy exact error text from the CI output — these go directly into issue bodies +- **Helix work item details**: Work item names, error snippets, and console log URLs - **Known issue matches** from Build Analysis - **The `[CI_ANALYSIS_SUMMARY]` JSON block** for structured analysis +**IMPORTANT**: Do not summarize or paraphrase error output. Copy the actual error messages, assertion failures, and stack traces verbatim from the CI analysis output. Issues must contain enough concrete detail for someone to understand the failure without re-running CI analysis. + ### Filtering Known Issues Skip failures that are already matched to known issues by Build Analysis. Focus only on **unknown/untracked failures** — these are the ones that need new issues. @@ -197,17 +202,35 @@ Create issues with the following structure: - **Pipeline**: - **Build**: [](https://dev.azure.com/dnceng-public/public/_build/results?buildId=) -- **Test**: `` +- **Failed Tests**: List each failing test with its fully qualified name - **Configuration**: - **Error Category**: +### Failing Tests + +List each failing test individually with its fully qualified name: + +| Test Name | Platform | Error Type | +|-----------|----------|------------| +| `Namespace.Class.Method` | linux-x64 | AssertionError / Timeout / Crash / etc. | + ### Error Output +Include the **actual error messages and stack traces** from the CI analysis output. +Do NOT write "Helix console logs are not accessible" — instead include whatever error +text the CI analysis script DID return (assertion messages, exit codes, error lines). +
Error details \`\`\` - + \`\`\`
@@ -253,6 +276,10 @@ Use filesystem-safe timestamp format `YYYY-MM-DD-HH-MM-SS` (no colons). - **Do not create duplicate issues.** Always search thoroughly before creating. - **Do not create issues for known/tracked failures.** If Build Analysis has already matched a failure to a known issue, skip it. - **Be conservative with "simple fix" assessments.** When in doubt, instruct Copilot to disable the test rather than attempt a fix. +- **Include specific test names and real error output in every issue.** Each issue MUST contain: + - Fully qualified test names (not just work item names like "GC" — drill into the specific test methods) + - Actual error messages, assertion text, or stack traces copied from the CI analysis output + - Do NOT say "Helix console logs are not accessible without authentication" as a substitute for error details. The CI analysis script already extracts error information — use it. - **Include enough context in issues** for Copilot Coding Agent to act without further investigation. - **Group related failures.** If the same test fails across multiple pipelines or configurations, create a single issue covering all occurrences. - If there are no new unknown failures to report, call the `noop` safe output explaining what you analyzed and that all failures are either known or already tracked. From 16069983ff40b3765e0f7ecfc9408dadcc6566f3 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:25:52 -0700 Subject: [PATCH 04/74] Add cross-module R2R reference resolution tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new test suite at src/tests/readytorun/crossmoduleresolution/ that tests R2R cross-module reference resolution across different assembly categories: Assembly graph: - A (main) - compilation target - B (version bubble, --inputbubbleref) - C (cross-module-inlineable only, --opt-cross-module) - D (external/transitive dependency) - E (type forwarder to D) Test scenarios cover: - TypeRef, MethodCall, FieldAccess across version bubble and cross-module-only - Transitive dependencies (C → D) - Nested types, type forwarders, mixed-origin generics - Interface dispatch with cross-module interfaces Two test modes via separate .csproj files: - main_crossmodule: --opt-cross-module:assemblyC (MutableModule #:N resolution) - main_bubble: --inputbubbleref:assemblyB.dll (MODULE_ZAPSIG encoding) Also adds CrossModuleResolutionTestPlan.md documenting all 5 planned phases including composite mode, ALC, and programmatic R2R validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CrossModuleResolutionTestPlan.md | 479 ++++++++++++++++++ .../crossmoduleresolution/README.md | 37 ++ .../crossmoduleresolution/assemblyB/B.cs | 17 + .../assemblyB/assemblyB.csproj | 8 + .../crossmoduleresolution/assemblyC/C.cs | 64 +++ .../assemblyC/assemblyC.csproj | 10 + .../crossmoduleresolution/assemblyD/D.cs | 30 ++ .../assemblyD/assemblyD.csproj | 8 + .../crossmoduleresolution/assemblyE/E.cs | 6 + .../assemblyE/assemblyE.csproj | 9 + .../crossmoduleresolution/main/main.cs | 117 +++++ .../main/main_bubble.csproj | 161 ++++++ .../main/main_crossmodule.csproj | 161 ++++++ 13 files changed, 1107 insertions(+) create mode 100644 CrossModuleResolutionTestPlan.md create mode 100644 src/tests/readytorun/crossmoduleresolution/README.md create mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyB/B.cs create mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyB/assemblyB.csproj create mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs create mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj create mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyD/D.cs create mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyD/assemblyD.csproj create mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyE/E.cs create mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyE/assemblyE.csproj create mode 100644 src/tests/readytorun/crossmoduleresolution/main/main.cs create mode 100644 src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj create mode 100644 src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj diff --git a/CrossModuleResolutionTestPlan.md b/CrossModuleResolutionTestPlan.md new file mode 100644 index 00000000000000..7d409f433b745c --- /dev/null +++ b/CrossModuleResolutionTestPlan.md @@ -0,0 +1,479 @@ +# Cross-Module Reference Resolution Tests for R2R + +## Problem Statement + +The MutableModule's cross-module reference resolution has limited test coverage, especially for: +- Different assembly categories (version bubble, cross-module-inlineable-only, external/transitive) +- Composite mode (where `#:N` ModuleRef resolution is blocked by `m_pILModule == NULL`) +- ALC interactions (custom ALCs, composite single-ALC enforcement, JIT fallback) +- Edge cases (nested types, type forwarders, mixed-origin generics, field/method refs) + +We need tests that cover every path through `GetNonNestedResolutionScope` and the corresponding +runtime resolution in `NativeManifestModule`, for both single-assembly and composite R2R modes. + +## Background: Five Assembly Categories + +| Category | In Compilation Set | In Version Bubble | CrossModuleInlineable | +|----------|:--:|:--:|:--:| +| A. Compilation Module | ✅ | ✅ | ✅ | +| B. Version Bubble (`--inputbubbleref`) | ❌ | ✅ | ✅ | +| C. CrossModule-Only (`--opt-cross-module`) | ❌ | ❌ | ✅ | +| D. External (transitive dep) | ❌ | ❌ | ❌ | +| E. CoreLib | Special | Special | Special | + +### Critical Decision Tree for Type Tokenization + +- `VersionsWithType(type) == true` → original TypeDef from type's module → `MODULE_ZAPSIG` in fixup → **works everywhere** +- `VersionsWithType(type) == false` → MutableModule creates TypeRef → resolution scope is ModuleRef: + - CoreLib → `"System.Private.CoreLib"` → **works everywhere** + - CrossModuleInlineable/VersionsWithModule → `#:N` → **works non-composite, fails composite** + - External → `#AssemblyName:N` → **works non-composite, fails composite** + +### Why `#:N` ModuleRef Instead of AssemblyRef + +`NativeManifestModule` is `ModuleBase` (not `Module`) — it has no `PEAssembly`, no `Assembly`, +no ALC binder. It cannot independently resolve AssemblyRefs. `LoadAssemblyImpl` throws +`COR_E_BADIMAGEFORMAT` unconditionally. The `#:N` format routes loading through `m_pILModule` +(a real Module with an ALC binder). The `#AssemblyName:N` format preserves ALC-chaining through +intermediate modules. + +--- + +## Existing Infrastructure + +### 1. `src/tests/readytorun/tests/mainv1.csproj` — Cross-module inlining pattern +- Multiple assemblies compiled with explicit crossgen2 precommands +- Uses `--opt-cross-module:test`, `--map`, custom ALC loading +- `CLRTestBatchPreCommands`/`CLRTestBashPreCommands` pattern + +### 2. `src/tests/readytorun/crossboundarylayout/` — Composite mode matrix +- Shell driver with composite/inputbubble/single mode permutations +- Focused on field layout, not cross-module references + +### 3. `ILCompiler.Reflection.ReadyToRun` — Programmatic R2R reader +- `ReadyToRunReader`: `Methods`, `ImportSections`, `ManifestReferenceAssemblies` +- `ReadyToRunMethod`: `Fixups` → `FixupCell` → `ReadyToRunSignature` +- `InliningInfoSection2`: Cross-module inlining records with module indices +- `ReadyToRunSignature.ToString()`: Renders MODULE_ZAPSIG, ModuleOverride + +### 4. R2RDump CLI +- `--header --sc` dumps section contents including InliningInfo +- `--in ` with `-r ` for resolving references +- Text output only (no JSON), but parseable + +### 5. `CLRTest.CrossGen.targets` auto-R2RDump +- Infrastructure runs `R2RDump --header --sc --val` after crossgen2 automatically + +--- + +## R2R Compilation Validation Infrastructure + +### Goal +Validate that cross-module inlining **actually occurred** and that the expected fixup +signatures reference the expected external modules. + +### What the `--map` flag does NOT do +The crossgen2 `--map` flag produces a symbol/section layout map (RVA, length, node type) — a +linker-style map. It does **not** contain fixup signature details, MODULE_OVERRIDE references, +or inlining information. Existing tests (mainv1) use it only to confirm crossgen2 ran successfully. + +### Approach: R2RDump for Compile-Time Validation + +R2RDump (located at `src/coreclr/tools/r2rdump/`) is a CLI tool that reads R2R images and dumps +their contents, including import sections, fixup signatures, and inlining info. It uses the +`ILCompiler.Reflection.ReadyToRun` library internally. + +#### Strategy 1: R2RDump Section Contents (compile-time, in precommands) +Run R2RDump with `--sc` (section contents) after crossgen2 to dump the `InliningInfo2` section. +Parse the text output to verify cross-module inliner/inlinee relationships with module indices. + +```bash +# Run R2RDump after crossgen2 in the precommands +"$CORE_ROOT"/crossgen2/r2rdump --in main.dll --sc --rp "$CORE_ROOT" --rp . > main.r2rdump + +# Verify the InliningInfo2 section contains cross-module entries referencing assemblyC +grep -q "module assemblyC" main.r2rdump || (echo "FAIL: no cross-module inlining from assemblyC" && exit 1) +``` + +The `InliningInfoSection2` decoder in R2RDump outputs lines like: +``` +Inliners for inlinee 06000003 (module assemblyC): + 06000001 +``` +This shows that a method from `assemblyC` was inlined into `main`, confirming cross-module +inlining occurred at compile time. + +#### Strategy 2: Runtime Correctness +Test methods call cross-module inlined code and verify return values. If fixups resolved +correctly, the methods return expected values. This proves end-to-end correctness. + +#### Strategy 3: Programmatic Validation via ILCompiler.Reflection.ReadyToRun (future Phase 5) +For deeper validation, a managed test can reference `ILCompiler.Reflection.ReadyToRun` and use: +- `ReadyToRunReader.ManifestReferenceAssemblies` — verify expected assemblies in manifest +- `ReadyToRunMethod.Fixups` → `FixupCell.Signature` → `ReadyToRunSignature.ToString()` — verify MODULE_OVERRIDE references +- `InliningInfoSection2` — verify cross-module inlining records with module indices + +This is more robust than text-parsing R2RDump output but requires building a managed validation +tool. Deferred to Phase 5. + +### Validation Points Per Test Mode + +| Validation | What It Proves | +|------------|---------------| +| R2RDump InliningInfo2 shows `module assemblyC` | Crossgen2 performed cross-module inlining | +| R2RDump ManifestMetadata lists assemblyC | Manifest AssemblyRef table includes the dependency | +| Test methods return correct values at runtime | Fixups resolved successfully end-to-end | +| Crossgen2 `--map` file exists | Crossgen2 ran successfully (basic sanity) | + +--- + +## Test Design + +### Assembly Graph + +``` +Assembly A (main, compilation target) +├── References B (version bubble, --inputbubbleref) +├── References C (cross-module-inlineable only, --opt-cross-module:assemblyC) +│ ├── C references D (external, transitive dependency) +│ └── C references CoreLib +└── References CoreLib + +Assembly B (version bubble) +├── Defines types for version-bubble testing +└── References CoreLib + +Assembly C (cross-module-inlineable, NOT in version bubble) +├── Defines inlineable methods that reference: +│ ├── C's own types +│ ├── D.DType (transitive dependency) +│ ├── D.Outer.Inner (nested type) +│ ├── CoreLib types (List, etc.) +│ └── Type forwarded types +└── References D + +Assembly D (external, NOT in any set) +├── Defines types referenced transitively through C +├── Defines nested types (D.Outer.Inner) +└── Defines types used as generic arguments + +Assembly E (type forwarder source) +├── Has TypeForwarder for SomeForwardedType → D +└── C references SomeForwardedType via E's forwarder +``` + +### File Structure + +``` +src/tests/readytorun/crossmoduleresolution/ +├── main/ +│ ├── main.cs — Test driver with all test methods +│ ├── main.csproj — Main project (basic build, no crossgen2) +│ ├── main_crossmodule.csproj — Single R2R with --opt-cross-module:assemblyC +│ └── main_bubble.csproj — Single R2R with --inputbubbleref assemblyB +├── assemblyB/ +│ ├── B.cs — Version bubble types +│ └── assemblyB.csproj +├── assemblyC/ +│ ├── C.cs — Cross-module-inlineable methods + types +│ └── assemblyC.csproj +├── assemblyD/ +│ ├── D.cs — External/transitive types, nested types +│ └── assemblyD.csproj +├── assemblyE/ +│ ├── E.cs — TypeForwarder assembly +│ └── assemblyE.csproj +└── README.md — Test documentation +``` + +### Assembly Source Code + +#### Assembly D (`assemblyD/D.cs`) +```csharp +namespace AssemblyD +{ + public class DType { public int Value => 42; } + + public class DClass + { + public static int StaticField = 100; + public static int StaticMethod() => StaticField + 1; + } + + public class Outer + { + public class Inner + { + public int GetValue() => 99; + } + } + + public class SomeForwardedType + { + public static string Name => "forwarded"; + } +} +``` + +#### Assembly E (`assemblyE/E.cs`) +```csharp +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(AssemblyD.SomeForwardedType))] +``` + +#### Assembly B (`assemblyB/B.cs`) +```csharp +namespace AssemblyB +{ + public class BType { public int Value => 7; } + + public class BClass + { + public static int StaticMethod() => 77; + public static int StaticField = 777; + } +} +``` + +#### Assembly C (`assemblyC/C.cs`) +```csharp +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace AssemblyC +{ + public class CType { public int Value => 3; } + + public class CClass + { + public static int StaticField = 50; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UseOwnType() => new CType().Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UseDType() => new AssemblyD.DType().Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CallDMethod() => AssemblyD.DClass.StaticMethod(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadDField() => AssemblyD.DClass.StaticField; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UseNestedType() => new AssemblyD.Outer.Inner().GetValue(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string UseForwardedType() => AssemblyD.SomeForwardedType.Name; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UseGenericWithDType() + { + var list = new List(); + list.Add(new AssemblyD.DType()); + return list[0].Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UseCoreLibGeneric() + { + var list = new List { 1, 2, 3 }; + return list.Count; + } + } + + public class CGeneric + { + public T Value { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetCount() => 1; + } + + public interface ICrossModule + { + int DoWork(); + } +} +``` + +#### Main Test Driver (`main/main.cs`) +```csharp +using System; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using Xunit; + +// Interface implementation for cross-module dispatch test +class CrossModuleImpl : AssemblyC.ICrossModule +{ + public int DoWork() => 42; +} + +public static class CrossModuleResolutionTests +{ + // --- Version Bubble Tests (B) --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestTypeRef_VersionBubble() => AssemblyB.BClass.StaticMethod(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestFieldAccess_VersionBubble() => AssemblyB.BClass.StaticField; + + // --- Cross-Module-Only Tests (C) --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestTypeRef_CrossModuleOnly() => AssemblyC.CClass.UseOwnType(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestTypeRef_Transitive() => AssemblyC.CClass.UseDType(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestMethodCall_Transitive() => AssemblyC.CClass.CallDMethod(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestFieldAccess_Transitive() => AssemblyC.CClass.ReadDField(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestNestedType_External() => AssemblyC.CClass.UseNestedType(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static string TestTypeForwarder() => AssemblyC.CClass.UseForwardedType(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestGeneric_MixedOrigin() => AssemblyC.CClass.UseGenericWithDType(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestGeneric_CoreLib() => AssemblyC.CClass.UseCoreLibGeneric(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestGeneric_CrossModuleDefinition() => AssemblyC.CGeneric.GetCount(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestFieldAccess_CrossModule() => AssemblyC.CClass.StaticField; + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestInterfaceDispatch_CrossModule() + { + AssemblyC.ICrossModule impl = new CrossModuleImpl(); + return impl.DoWork(); + } + + // --- ALC Tests --- + + class TestLoadContext : AssemblyLoadContext + { + public TestLoadContext() : base( + AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()).IsCollectible) + { } + + public void TestLoadInSeparateALC() + { + // Load main assembly in a different ALC — R2R should still work (or JIT fallback) + Assembly a = LoadFromAssemblyPath( + Path.Combine(Directory.GetCurrentDirectory(), "main.dll")); + Assert.AreEqual(GetLoadContext(a), this); + } + + protected override Assembly Load(AssemblyName an) => null; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TestALC_CustomLoad() => new TestLoadContext().TestLoadInSeparateALC(); + + // --- Entry Point --- + + [Fact] + public static int TestEntryPoint() + { + // Version bubble + Assert.AreEqual(77, TestTypeRef_VersionBubble()); + Assert.AreEqual(777, TestFieldAccess_VersionBubble()); + + // Cross-module-only (C's inlined methods) + Assert.AreEqual(3, TestTypeRef_CrossModuleOnly()); + Assert.AreEqual(42, TestTypeRef_Transitive()); + Assert.AreEqual(101, TestMethodCall_Transitive()); + Assert.AreEqual(100, TestFieldAccess_Transitive()); + Assert.AreEqual(99, TestNestedType_External()); + Assert.AreEqual("forwarded", TestTypeForwarder()); + Assert.AreEqual(42, TestGeneric_MixedOrigin()); + Assert.AreEqual(3, TestGeneric_CoreLib()); + Assert.AreEqual(1, TestGeneric_CrossModuleDefinition()); + Assert.AreEqual(50, TestFieldAccess_CrossModule()); + Assert.AreEqual(42, TestInterfaceDispatch_CrossModule()); + + // ALC + TestALC_CustomLoad(); + + return 100; // success + } +} +``` + +### Test Modes (Separate .csproj Files) + +#### Mode 1: `main_crossmodule.csproj` — Single R2R + `--opt-cross-module` + +Crossgen2 precommands: +1. Copy IL DLLs to `IL_DLLS/` +2. Crossgen2 assemblyD (no special flags) +3. Crossgen2 assemblyB (no special flags) +4. Crossgen2 assemblyC with `-r assemblyD.dll` +5. Crossgen2 main with `--opt-cross-module:assemblyC --map -r assemblyB.dll -r assemblyC.dll -r assemblyD.dll` +6. **Validate**: check map file for MODULE_OVERRIDE references to assemblyC + +#### Mode 2: `main_bubble.csproj` — Single R2R + `--inputbubbleref` + +Crossgen2 precommands: +1. Copy IL DLLs to `IL_DLLS/` +2. Crossgen2 assemblyB (no special flags) +3. Crossgen2 main with `--inputbubbleref assemblyB --map -r assemblyB.dll -r assemblyC.dll -r assemblyD.dll` +4. **Validate**: check map file for MODULE_ZAPSIG references to assemblyB + +--- + +## Phases + +### Phase 1: Scaffolding (Current Scope) +1. Create Assembly D — external types, nested types, forwarded type definition +2. Create Assembly E — TypeForwarder to D +3. Create Assembly B — version bubble types +4. Create Assembly C — cross-module-inlineable methods with `[AggressiveInlining]` +5. Create main test driver with all test methods + +### Phase 2: Single-Assembly R2R Tests (Current Scope) +6. Create `main_crossmodule.csproj` with crossgen2 precommands + map file validation +7. Create `main_bubble.csproj` with crossgen2 precommands + map file validation +8. Build and run — verify all tests pass in single R2R mode + +### Phase 3: Composite Mode Tests (Deferred) +9. Create `main_composite.csproj` — `--composite` with A+B +10. Create `main_composite_crossmodule.csproj` — `--composite` A+B + `--opt-cross-module:assemblyC` +11. Determine expected behavior — should composite+crossmodule fail at crossgen2 time, at runtime, or JIT fallback? +12. Build and run — verify composite tests + +### Phase 4: ALC Tests (Deferred) +13. Add ALC test cases — custom ALC loading, composite ALC mismatch fallback to JIT +14. Build and run — verify ALC scenarios + +### Phase 5: Programmatic R2R Validation (Deferred) +15. Create managed validation tool using `ILCompiler.Reflection.ReadyToRun` +16. Validate `ManifestReferenceAssemblies` contains expected assemblies +17. Validate `ReadyToRunMethod.Fixups` contain expected MODULE_OVERRIDE signatures +18. Validate `InliningInfoSection2` records cross-module inlining with correct module indices + +--- + +## Design Decisions + +- **AggressiveInlining**: Yes — `[MethodImpl(AggressiveInlining)]` on C's methods to force cross-module inlining +- **Composite + cross-module behavior**: Eventually should be fixed (probably JIT fallback). Deferred to Phase 3 +- **Test organization**: Separate .csproj files per mode (mainv1/mainv2 pattern) +- **Pre-commands**: Both Windows batch AND bash (following existing convention) +- **Priority**: Pri1 — specialized cross-module tests +- **Validation**: Map file + R2RDump for Phase 1-2; programmatic ILCompiler.Reflection.ReadyToRun for Phase 5 +- **Return code**: 100 = success (matching CoreCLR test convention) diff --git a/src/tests/readytorun/crossmoduleresolution/README.md b/src/tests/readytorun/crossmoduleresolution/README.md new file mode 100644 index 00000000000000..8b78a0615680f5 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/README.md @@ -0,0 +1,37 @@ +# Cross-Module Resolution Tests + +These tests verify R2R (ReadyToRun) cross-module reference resolution across different +assembly categories and crossgen2 compilation modes. + +## Assembly Graph + +- **Assembly A** (`main/`) — Main test driver, compilation target +- **Assembly B** (`assemblyB/`) — Version bubble (`--inputbubbleref`) +- **Assembly C** (`assemblyC/`) — Cross-module-inlineable only (`--opt-cross-module`) +- **Assembly D** (`assemblyD/`) — External/transitive dependency (not in any set) +- **Assembly E** (`assemblyE/`) — TypeForwarder assembly (forwards to D) + +## Test Modes + +| Variant | Crossgen2 Flags | What It Tests | +|---------|----------------|---------------| +| `main_crossmodule` | `--opt-cross-module:assemblyC` | MutableModule `#:N` and `#D:N` ModuleRef resolution | +| `main_bubble` | `--inputbubbleref:assemblyB.dll` | Version bubble MODULE_ZAPSIG encoding | + +## Test Cases + +Each test method exercises a different cross-module reference scenario: +- TypeRef from version bubble (B) and cross-module-only (C) +- Method calls and field accesses across module boundaries +- Transitive dependencies (C → D) +- Nested types, type forwarders, mixed-origin generics +- Interface dispatch with cross-module interfaces +- Custom ALC loading + +## Building + +These are pri1 tests. Build with `-priority1`: + +```bash +src/tests/build.sh -Test src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj x64 Release -priority1 +``` diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyB/B.cs b/src/tests/readytorun/crossmoduleresolution/assemblyB/B.cs new file mode 100644 index 00000000000000..34020f62ebc0ea --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/assemblyB/B.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace AssemblyB +{ + public class BType + { + public int Value => 7; + } + + public class BClass + { + public static int StaticField = 777; + + public static int StaticMethod() => 77; + } +} diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyB/assemblyB.csproj b/src/tests/readytorun/crossmoduleresolution/assemblyB/assemblyB.csproj new file mode 100644 index 00000000000000..e2440d3c0c8474 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/assemblyB/assemblyB.csproj @@ -0,0 +1,8 @@ + + + library + + + + + diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs b/src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs new file mode 100644 index 00000000000000..e6edc45cf323b9 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace AssemblyC +{ + public class CType + { + public int Value => 3; + } + + public class CClass + { + public static int StaticField = 50; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UseOwnType() => new CType().Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UseDType() => new AssemblyD.DType().Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CallDMethod() => AssemblyD.DClass.StaticMethod(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadDField() => AssemblyD.DClass.StaticField; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UseNestedType() => new AssemblyD.Outer.Inner().GetValue(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string UseForwardedType() => AssemblyD.SomeForwardedType.Name; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UseGenericWithDType() + { + var list = new List(); + list.Add(new AssemblyD.DType()); + return list[0].Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UseCoreLibGeneric() + { + var list = new List { 1, 2, 3 }; + return list.Count; + } + } + + public class CGeneric + { + public T Value { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetCount() => 1; + } + + public interface ICrossModule + { + int DoWork(); + } +} diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj b/src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj new file mode 100644 index 00000000000000..edcaf77d0efa1a --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj @@ -0,0 +1,10 @@ + + + library + + + + + + + diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyD/D.cs b/src/tests/readytorun/crossmoduleresolution/assemblyD/D.cs new file mode 100644 index 00000000000000..a3869a6d2d1c57 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/assemblyD/D.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace AssemblyD +{ + public class DType + { + public int Value => 42; + } + + public class DClass + { + public static int StaticField = 100; + + public static int StaticMethod() => StaticField + 1; + } + + public class Outer + { + public class Inner + { + public int GetValue() => 99; + } + } + + public class SomeForwardedType + { + public static string Name => "forwarded"; + } +} diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyD/assemblyD.csproj b/src/tests/readytorun/crossmoduleresolution/assemblyD/assemblyD.csproj new file mode 100644 index 00000000000000..155932bd710178 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/assemblyD/assemblyD.csproj @@ -0,0 +1,8 @@ + + + library + + + + + diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyE/E.cs b/src/tests/readytorun/crossmoduleresolution/assemblyE/E.cs new file mode 100644 index 00000000000000..56e99beae7a1e6 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/assemblyE/E.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(AssemblyD.SomeForwardedType))] diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyE/assemblyE.csproj b/src/tests/readytorun/crossmoduleresolution/assemblyE/assemblyE.csproj new file mode 100644 index 00000000000000..4de3dc21b44be9 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/assemblyE/assemblyE.csproj @@ -0,0 +1,9 @@ + + + library + + + + + + diff --git a/src/tests/readytorun/crossmoduleresolution/main/main.cs b/src/tests/readytorun/crossmoduleresolution/main/main.cs new file mode 100644 index 00000000000000..17c68633f01352 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/main/main.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; + +public static class Assert +{ + public static bool HasAssertFired; + + public static void AreEqual(object actual, object expected) + { + if (!(actual is null && expected is null) && !actual.Equals(expected)) + { + Console.WriteLine("Not equal!"); + Console.WriteLine("actual = " + actual.ToString()); + Console.WriteLine("expected = " + expected.ToString()); + HasAssertFired = true; + } + } +} + +class CrossModuleImpl : AssemblyC.ICrossModule +{ + public int DoWork() => 42; +} + +class Program +{ + // --- Version Bubble Tests (B) --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestTypeRef_VersionBubble() => new AssemblyB.BType().Value; + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestMethodCall_VersionBubble() => AssemblyB.BClass.StaticMethod(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestFieldAccess_VersionBubble() => AssemblyB.BClass.StaticField; + + // --- Cross-Module-Only Tests (C → inlined into main) --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestTypeRef_CrossModuleOwn() => AssemblyC.CClass.UseOwnType(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestTypeRef_Transitive() => AssemblyC.CClass.UseDType(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestMethodCall_Transitive() => AssemblyC.CClass.CallDMethod(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestFieldAccess_Transitive() => AssemblyC.CClass.ReadDField(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestNestedType_External() => AssemblyC.CClass.UseNestedType(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static string TestTypeForwarder() => AssemblyC.CClass.UseForwardedType(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestGeneric_MixedOrigin() => AssemblyC.CClass.UseGenericWithDType(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestGeneric_CoreLib() => AssemblyC.CClass.UseCoreLibGeneric(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestGeneric_CrossModuleDefinition() => AssemblyC.CGeneric.GetCount(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestFieldAccess_CrossModule() => AssemblyC.CClass.StaticField; + + [MethodImpl(MethodImplOptions.NoInlining)] + static int TestInterfaceDispatch_CrossModule() + { + AssemblyC.ICrossModule impl = new CrossModuleImpl(); + return impl.DoWork(); + } + + static void RunAllTests() + { + // Version bubble tests + Assert.AreEqual(TestTypeRef_VersionBubble(), 7); + Assert.AreEqual(TestMethodCall_VersionBubble(), 77); + Assert.AreEqual(TestFieldAccess_VersionBubble(), 777); + + // Cross-module-only tests (C's AggressiveInlining methods) + Assert.AreEqual(TestTypeRef_CrossModuleOwn(), 3); + Assert.AreEqual(TestTypeRef_Transitive(), 42); + Assert.AreEqual(TestMethodCall_Transitive(), 101); + Assert.AreEqual(TestFieldAccess_Transitive(), 100); + Assert.AreEqual(TestNestedType_External(), 99); + Assert.AreEqual(TestTypeForwarder(), "forwarded"); + Assert.AreEqual(TestGeneric_MixedOrigin(), 42); + Assert.AreEqual(TestGeneric_CoreLib(), 3); + Assert.AreEqual(TestGeneric_CrossModuleDefinition(), 1); + Assert.AreEqual(TestFieldAccess_CrossModule(), 50); + Assert.AreEqual(TestInterfaceDispatch_CrossModule(), 42); + } + + public static int Main() + { + // Run all tests 3x to exercise both slow and fast paths + for (int i = 0; i < 3; i++) + RunAllTests(); + + if (!Assert.HasAssertFired) + Console.WriteLine("PASSED"); + else + Console.WriteLine("FAILED"); + + return Assert.HasAssertFired ? 1 : 100; + } +} diff --git a/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj b/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj new file mode 100644 index 00000000000000..36bf89f0210dc9 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj @@ -0,0 +1,161 @@ + + + true + false + false + 1 + true + + true + + + + + + + + + + + + nul + +REM Copy IL assemblies before crossgen2 overwrites them +for %%A in (main_bubble assemblyB assemblyC assemblyD assemblyE) do ( + if not exist IL_DLLS\%%A.dll ( + copy /y %%A.dll IL_DLLS\%%A.dll + if not exist IL_DLLS\%%A.dll ( + echo FAILED to copy %%A.dll to IL_DLLS + exit /b 1 + ) + ) +) + +REM Crossgen2 assemblyD (no special flags) +%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -o:assemblyD.dll IL_DLLS\assemblyD.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen assemblyD failed with exitcode - !CrossGenStatus! + Exit /b 1 +) + +REM Crossgen2 assemblyE (references assemblyD) +%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS\assemblyE.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen assemblyE failed with exitcode - !CrossGenStatus! + Exit /b 1 +) + +REM Crossgen2 assemblyB (no special flags) +%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -o:assemblyB.dll IL_DLLS\assemblyB.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen assemblyB failed with exitcode - !CrossGenStatus! + Exit /b 1 +) + +REM Crossgen2 assemblyC (references assemblyD, assemblyE) +%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS\assemblyC.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen assemblyC failed with exitcode - !CrossGenStatus! + Exit /b 1 +) + +REM Crossgen2 main with --inputbubbleref assemblyB (version bubble) +%Core_Root%\crossgen2\crossgen2.exe --map -r:%Core_Root%\*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --inputbubbleref:assemblyB.dll -o:main_bubble.dll IL_DLLS\main_bubble.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen main_bubble failed with exitcode - !CrossGenStatus! + Exit /b 1 +) +if not exist main_bubble.map ( + echo FAILED to build main_bubble.dll - no map file + exit /b 1 +) + +endlocal +]]> + + + diff --git a/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj b/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj new file mode 100644 index 00000000000000..6ce20a0fd43c9c --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj @@ -0,0 +1,161 @@ + + + true + false + false + 1 + true + + true + + + + + + + + + + + + nul + +REM Copy IL assemblies before crossgen2 overwrites them +for %%A in (main_crossmodule assemblyB assemblyC assemblyD assemblyE) do ( + if not exist IL_DLLS\%%A.dll ( + copy /y %%A.dll IL_DLLS\%%A.dll + if not exist IL_DLLS\%%A.dll ( + echo FAILED to copy %%A.dll to IL_DLLS + exit /b 1 + ) + ) +) + +REM Crossgen2 assemblyD (no special flags) +%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -o:assemblyD.dll IL_DLLS\assemblyD.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen assemblyD failed with exitcode - !CrossGenStatus! + Exit /b 1 +) + +REM Crossgen2 assemblyE (references assemblyD) +%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS\assemblyE.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen assemblyE failed with exitcode - !CrossGenStatus! + Exit /b 1 +) + +REM Crossgen2 assemblyB (no special flags) +%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -o:assemblyB.dll IL_DLLS\assemblyB.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen assemblyB failed with exitcode - !CrossGenStatus! + Exit /b 1 +) + +REM Crossgen2 assemblyC (references assemblyD, assemblyE) +%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS\assemblyC.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen assemblyC failed with exitcode - !CrossGenStatus! + Exit /b 1 +) + +REM Crossgen2 main with --opt-cross-module:assemblyC +%Core_Root%\crossgen2\crossgen2.exe --map -r:%Core_Root%\*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --opt-cross-module:assemblyC -o:main_crossmodule.dll IL_DLLS\main_crossmodule.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen main_crossmodule failed with exitcode - !CrossGenStatus! + Exit /b 1 +) +if not exist main_crossmodule.map ( + echo FAILED to build main_crossmodule.dll - no map file + exit /b 1 +) + +endlocal +]]> + + + From 14937283544ead495e600caba13d6cd7158a0f82 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:45:58 -0700 Subject: [PATCH 05/74] Add runtime-async method variants to cross-module resolution tests Add async Task and async Task variants of each cross-module inlineable method in assemblyC, with corresponding test methods in main.cs. Enable runtime-async compilation via Features=runtime-async=on and --opt-async-methods crossgen2 flag in both test projects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Composite Mode Async Thunks in R2R.md | 28 ++++++++++ .../crossmoduleresolution/assemblyC/C.cs | 51 +++++++++++++++++ .../assemblyC/assemblyC.csproj | 2 + .../crossmoduleresolution/main/main.cs | 56 +++++++++++++++++++ .../main/main_bubble.csproj | 22 ++++---- .../main/main_crossmodule.csproj | 22 ++++---- 6 files changed, 161 insertions(+), 20 deletions(-) create mode 100644 Composite Mode Async Thunks in R2R.md diff --git a/Composite Mode Async Thunks in R2R.md b/Composite Mode Async Thunks in R2R.md new file mode 100644 index 00000000000000..e84a55408c15f6 --- /dev/null +++ b/Composite Mode Async Thunks in R2R.md @@ -0,0 +1,28 @@ +# Composite Mode Async Thunks in R2R + +We unconditionally wrap MethodIL in MutableModuleWrappedMethodIL. + +At runtime, we disable any tokens pointing from MutableModule to any Module except for SPCL + +We also can't avoid creating MutableModule tokens for any TypeSystemEntities in the composite image. We inject methods +like Task.FromResult(T obj) where T could be from within the composite image. This would necessitate a new entry in +the MutableModule for the generic method with the definition pointing to SPCL, but the generic argument a token within +the composite image. + +We should start by enabling the AsyncVariantMethod method bodies. The real issue is the thunks, not the Async methods. + +After that I don't see a way forward to emit these thunks without enabling MutableModule references within the composite +image version bubble. This was explicitly forbidden in the original implementation for cross-module inlining, but I +don't exactly know why. If we can find exactly why (and hopefully it's not a hard restriction that can't be worked +around), we can build safeguards and tests to validate the safety of it. + +One alternative could be a special fixup that has the info required to construct the type without requiring module +tokens. Though a natural next step would be to deduplicate this information, which starts to look a lot like the +MutableModule. + +## Issue: How do we know we can resolve a type or method in different situations + +We may have Assembly A loaded and are doing eager fixups as we load Assembly B, then have a reference to a type in +Assembly D which goes through Assembly C. Do we need to load both assembly C and D? Are we able to do that in an eager +fixup while we load Assembly B? These are the types of issues we find + diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs b/src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs index e6edc45cf323b9..c3f42b474f31e6 100644 --- a/src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs +++ b/src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Threading.Tasks; namespace AssemblyC { @@ -47,6 +48,56 @@ public static int UseCoreLibGeneric() var list = new List { 1, 2, 3 }; return list.Count; } + + // --- Async variants (runtime-async thunk targets) --- + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task UseOwnTypeAsync() => new CType().Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task UseDTypeAsync() => new AssemblyD.DType().Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task CallDMethodAsync() => AssemblyD.DClass.StaticMethod(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task ReadDFieldAsync() => AssemblyD.DClass.StaticField; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task UseNestedTypeAsync() => new AssemblyD.Outer.Inner().GetValue(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task UseForwardedTypeAsync() => AssemblyD.SomeForwardedType.Name; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task UseGenericWithDTypeAsync() + { + var list = new List(); + list.Add(new AssemblyD.DType()); + return list[0].Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task UseCoreLibGenericAsync() + { + var list = new List { 1, 2, 3 }; + return list.Count; + } + + // Task-returning (void-equivalent) async variants + public static int AsyncSideEffect; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task UseOwnTypeAsyncVoid() + { + AsyncSideEffect = new CType().Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task UseDTypeAsyncVoid() + { + AsyncSideEffect = new AssemblyD.DType().Value; + } } public class CGeneric diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj b/src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj index edcaf77d0efa1a..6ea9a5266546ec 100644 --- a/src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj +++ b/src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj @@ -1,6 +1,8 @@ library + $(Features);runtime-async=on + $(NoWarn);CS1998 diff --git a/src/tests/readytorun/crossmoduleresolution/main/main.cs b/src/tests/readytorun/crossmoduleresolution/main/main.cs index 17c68633f01352..ca81473480837c 100644 --- a/src/tests/readytorun/crossmoduleresolution/main/main.cs +++ b/src/tests/readytorun/crossmoduleresolution/main/main.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Loader; +using System.Threading.Tasks; public static class Assert { @@ -80,6 +81,45 @@ static int TestInterfaceDispatch_CrossModule() return impl.DoWork(); } + // --- Async Cross-Module Tests (runtime-async thunks) --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task TestAsyncTypeRef_CrossModuleOwn() => await AssemblyC.CClass.UseOwnTypeAsync(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task TestAsyncTypeRef_Transitive() => await AssemblyC.CClass.UseDTypeAsync(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task TestAsyncMethodCall_Transitive() => await AssemblyC.CClass.CallDMethodAsync(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task TestAsyncFieldAccess_Transitive() => await AssemblyC.CClass.ReadDFieldAsync(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task TestAsyncNestedType_External() => await AssemblyC.CClass.UseNestedTypeAsync(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task TestAsyncTypeForwarder() => await AssemblyC.CClass.UseForwardedTypeAsync(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task TestAsyncGeneric_MixedOrigin() => await AssemblyC.CClass.UseGenericWithDTypeAsync(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task TestAsyncGeneric_CoreLib() => await AssemblyC.CClass.UseCoreLibGenericAsync(); + + // Task-returning (void-equivalent) async variants + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task TestAsyncVoid_CrossModuleOwn() + { + await AssemblyC.CClass.UseOwnTypeAsyncVoid(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task TestAsyncVoid_Transitive() + { + await AssemblyC.CClass.UseDTypeAsyncVoid(); + } + static void RunAllTests() { // Version bubble tests @@ -99,6 +139,22 @@ static void RunAllTests() Assert.AreEqual(TestGeneric_CrossModuleDefinition(), 1); Assert.AreEqual(TestFieldAccess_CrossModule(), 50); Assert.AreEqual(TestInterfaceDispatch_CrossModule(), 42); + + // Async cross-module tests (runtime-async thunks) + Assert.AreEqual(TestAsyncTypeRef_CrossModuleOwn().Result, 3); + Assert.AreEqual(TestAsyncTypeRef_Transitive().Result, 42); + Assert.AreEqual(TestAsyncMethodCall_Transitive().Result, 101); + Assert.AreEqual(TestAsyncFieldAccess_Transitive().Result, 100); + Assert.AreEqual(TestAsyncNestedType_External().Result, 99); + Assert.AreEqual(TestAsyncTypeForwarder().Result, "forwarded"); + Assert.AreEqual(TestAsyncGeneric_MixedOrigin().Result, 42); + Assert.AreEqual(TestAsyncGeneric_CoreLib().Result, 3); + + // Task-returning (void-equivalent) async tests + TestAsyncVoid_CrossModuleOwn().Wait(); + Assert.AreEqual(AssemblyC.CClass.AsyncSideEffect, 3); + TestAsyncVoid_Transitive().Wait(); + Assert.AreEqual(AssemblyC.CClass.AsyncSideEffect, 42); } public static int Main() diff --git a/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj b/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj index 36bf89f0210dc9..6d6fc4987da298 100644 --- a/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj +++ b/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj @@ -5,6 +5,8 @@ false 1 true + $(Features);runtime-async=on + $(NoWarn);CS1998 true @@ -42,7 +44,7 @@ for %%A in (main_bubble assemblyB assemblyC assemblyD assemblyE) do ( ) REM Crossgen2 assemblyD (no special flags) -%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -o:assemblyD.dll IL_DLLS\assemblyD.dll +%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -o:assemblyD.dll IL_DLLS\assemblyD.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -51,7 +53,7 @@ IF NOT !CrossGenStatus!==0 ( ) REM Crossgen2 assemblyE (references assemblyD) -%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS\assemblyE.dll +%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS\assemblyE.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -60,7 +62,7 @@ IF NOT !CrossGenStatus!==0 ( ) REM Crossgen2 assemblyB (no special flags) -%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -o:assemblyB.dll IL_DLLS\assemblyB.dll +%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -o:assemblyB.dll IL_DLLS\assemblyB.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -69,7 +71,7 @@ IF NOT !CrossGenStatus!==0 ( ) REM Crossgen2 assemblyC (references assemblyD, assemblyE) -%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS\assemblyC.dll +%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS\assemblyC.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -78,7 +80,7 @@ IF NOT !CrossGenStatus!==0 ( ) REM Crossgen2 main with --inputbubbleref assemblyB (version bubble) -%Core_Root%\crossgen2\crossgen2.exe --map -r:%Core_Root%\*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --inputbubbleref:assemblyB.dll -o:main_bubble.dll IL_DLLS\main_bubble.dll +%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods --map -r:%Core_Root%\*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --inputbubbleref:assemblyB.dll -o:main_bubble.dll IL_DLLS\main_bubble.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -112,7 +114,7 @@ for asm in main_bubble assemblyB assemblyC assemblyD assemblyE; do done # Crossgen2 assemblyD (no special flags) -"$CORE_ROOT"/crossgen2/crossgen2 -r:"$CORE_ROOT"/*.dll -o:assemblyD.dll IL_DLLS/assemblyD.dll +"$CORE_ROOT"/crossgen2/crossgen2 --opt-async-methods -r:"$CORE_ROOT"/*.dll -o:assemblyD.dll IL_DLLS/assemblyD.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ]; then echo "Crossgen assemblyD failed with exitcode: $__cgExitCode" @@ -120,7 +122,7 @@ if [ $__cgExitCode -ne 0 ]; then fi # Crossgen2 assemblyE (references assemblyD) -"$CORE_ROOT"/crossgen2/crossgen2 -r:"$CORE_ROOT"/*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS/assemblyE.dll +"$CORE_ROOT"/crossgen2/crossgen2 --opt-async-methods -r:"$CORE_ROOT"/*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS/assemblyE.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ]; then echo "Crossgen assemblyE failed with exitcode: $__cgExitCode" @@ -128,7 +130,7 @@ if [ $__cgExitCode -ne 0 ]; then fi # Crossgen2 assemblyB (no special flags) -"$CORE_ROOT"/crossgen2/crossgen2 -r:"$CORE_ROOT"/*.dll -o:assemblyB.dll IL_DLLS/assemblyB.dll +"$CORE_ROOT"/crossgen2/crossgen2 --opt-async-methods -r:"$CORE_ROOT"/*.dll -o:assemblyB.dll IL_DLLS/assemblyB.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ]; then echo "Crossgen assemblyB failed with exitcode: $__cgExitCode" @@ -136,7 +138,7 @@ if [ $__cgExitCode -ne 0 ]; then fi # Crossgen2 assemblyC (references assemblyD, assemblyE) -"$CORE_ROOT"/crossgen2/crossgen2 -r:"$CORE_ROOT"/*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS/assemblyC.dll +"$CORE_ROOT"/crossgen2/crossgen2 --opt-async-methods -r:"$CORE_ROOT"/*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS/assemblyC.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ]; then echo "Crossgen assemblyC failed with exitcode: $__cgExitCode" @@ -144,7 +146,7 @@ if [ $__cgExitCode -ne 0 ]; then fi # Crossgen2 main with --inputbubbleref assemblyB (version bubble) -"$CORE_ROOT"/crossgen2/crossgen2 --map -r:"$CORE_ROOT"/*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --inputbubbleref:assemblyB.dll -o:main_bubble.dll IL_DLLS/main_bubble.dll +"$CORE_ROOT"/crossgen2/crossgen2 --opt-async-methods --map -r:"$CORE_ROOT"/*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --inputbubbleref:assemblyB.dll -o:main_bubble.dll IL_DLLS/main_bubble.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ]; then echo "Crossgen main_bubble failed with exitcode: $__cgExitCode" diff --git a/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj b/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj index 6ce20a0fd43c9c..a0d01bb3168d72 100644 --- a/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj +++ b/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj @@ -5,6 +5,8 @@ false 1 true + $(Features);runtime-async=on + $(NoWarn);CS1998 true @@ -42,7 +44,7 @@ for %%A in (main_crossmodule assemblyB assemblyC assemblyD assemblyE) do ( ) REM Crossgen2 assemblyD (no special flags) -%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -o:assemblyD.dll IL_DLLS\assemblyD.dll +%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -o:assemblyD.dll IL_DLLS\assemblyD.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -51,7 +53,7 @@ IF NOT !CrossGenStatus!==0 ( ) REM Crossgen2 assemblyE (references assemblyD) -%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS\assemblyE.dll +%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS\assemblyE.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -60,7 +62,7 @@ IF NOT !CrossGenStatus!==0 ( ) REM Crossgen2 assemblyB (no special flags) -%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -o:assemblyB.dll IL_DLLS\assemblyB.dll +%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -o:assemblyB.dll IL_DLLS\assemblyB.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -69,7 +71,7 @@ IF NOT !CrossGenStatus!==0 ( ) REM Crossgen2 assemblyC (references assemblyD, assemblyE) -%Core_Root%\crossgen2\crossgen2.exe -r:%Core_Root%\*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS\assemblyC.dll +%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS\assemblyC.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -78,7 +80,7 @@ IF NOT !CrossGenStatus!==0 ( ) REM Crossgen2 main with --opt-cross-module:assemblyC -%Core_Root%\crossgen2\crossgen2.exe --map -r:%Core_Root%\*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --opt-cross-module:assemblyC -o:main_crossmodule.dll IL_DLLS\main_crossmodule.dll +%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods --map -r:%Core_Root%\*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --opt-cross-module:assemblyC -o:main_crossmodule.dll IL_DLLS\main_crossmodule.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -112,7 +114,7 @@ for asm in main_crossmodule assemblyB assemblyC assemblyD assemblyE; do done # Crossgen2 assemblyD (no special flags) -"$CORE_ROOT"/crossgen2/crossgen2 -r:"$CORE_ROOT"/*.dll -o:assemblyD.dll IL_DLLS/assemblyD.dll +"$CORE_ROOT"/crossgen2/crossgen2 --opt-async-methods -r:"$CORE_ROOT"/*.dll -o:assemblyD.dll IL_DLLS/assemblyD.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ]; then echo "Crossgen assemblyD failed with exitcode: $__cgExitCode" @@ -120,7 +122,7 @@ if [ $__cgExitCode -ne 0 ]; then fi # Crossgen2 assemblyE (references assemblyD) -"$CORE_ROOT"/crossgen2/crossgen2 -r:"$CORE_ROOT"/*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS/assemblyE.dll +"$CORE_ROOT"/crossgen2/crossgen2 --opt-async-methods -r:"$CORE_ROOT"/*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS/assemblyE.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ]; then echo "Crossgen assemblyE failed with exitcode: $__cgExitCode" @@ -128,7 +130,7 @@ if [ $__cgExitCode -ne 0 ]; then fi # Crossgen2 assemblyB (no special flags) -"$CORE_ROOT"/crossgen2/crossgen2 -r:"$CORE_ROOT"/*.dll -o:assemblyB.dll IL_DLLS/assemblyB.dll +"$CORE_ROOT"/crossgen2/crossgen2 --opt-async-methods -r:"$CORE_ROOT"/*.dll -o:assemblyB.dll IL_DLLS/assemblyB.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ]; then echo "Crossgen assemblyB failed with exitcode: $__cgExitCode" @@ -136,7 +138,7 @@ if [ $__cgExitCode -ne 0 ]; then fi # Crossgen2 assemblyC (references assemblyD, assemblyE) -"$CORE_ROOT"/crossgen2/crossgen2 -r:"$CORE_ROOT"/*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS/assemblyC.dll +"$CORE_ROOT"/crossgen2/crossgen2 --opt-async-methods -r:"$CORE_ROOT"/*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS/assemblyC.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ]; then echo "Crossgen assemblyC failed with exitcode: $__cgExitCode" @@ -144,7 +146,7 @@ if [ $__cgExitCode -ne 0 ]; then fi # Crossgen2 main with --opt-cross-module:assemblyC -"$CORE_ROOT"/crossgen2/crossgen2 --map -r:"$CORE_ROOT"/*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --opt-cross-module:assemblyC -o:main_crossmodule.dll IL_DLLS/main_crossmodule.dll +"$CORE_ROOT"/crossgen2/crossgen2 --opt-async-methods --map -r:"$CORE_ROOT"/*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --opt-cross-module:assemblyC -o:main_crossmodule.dll IL_DLLS/main_crossmodule.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ]; then echo "Crossgen main_crossmodule failed with exitcode: $__cgExitCode" From 70ad8f07e60f6b9a909de86d9f0770a7ef9d213e Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:49:00 -0700 Subject: [PATCH 06/74] Add R2R validation tool for cross-module resolution tests Add r2rvalidate tool using ILCompiler.Reflection.ReadyToRun to programmatically verify R2R compilation artifacts: - Assembly references (MSIL + manifest metadata AssemblyRef tables) - CHECK_IL_BODY fixups proving cross-module inlining occurred - RuntimeFunction counts confirming async thunk generation (3+) Integrate validator into test precommands for both main_crossmodule and main_bubble test variants. Validator runs after crossgen2 and before the actual test execution. For non-composite images, reads MSIL AssemblyRefs via GetGlobalMetadata().MetadataReader (not ManifestReferenceAssemblies which only holds extra manifest-level refs). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../main/main_bubble.csproj | 30 ++ .../main/main_crossmodule.csproj | 31 ++ .../r2rvalidate/R2RValidate.cs | 303 ++++++++++++++++++ .../r2rvalidate/r2rvalidate.csproj | 14 + 4 files changed, 378 insertions(+) create mode 100644 src/tests/readytorun/crossmoduleresolution/r2rvalidate/R2RValidate.cs create mode 100644 src/tests/readytorun/crossmoduleresolution/r2rvalidate/r2rvalidate.csproj diff --git a/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj b/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj index 6d6fc4987da298..a0676ac4963d6e 100644 --- a/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj +++ b/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj @@ -92,6 +92,21 @@ if not exist main_bubble.map ( exit /b 1 ) +REM R2R Validation: verify assembly references present (no cross-module inlining expected) +set R2RVALIDATE_DIR=..\..\r2rvalidate\r2rvalidate +if exist "%R2RVALIDATE_DIR%\r2rvalidate.dll" ( + echo Running R2R validation on main_bubble.dll... + %Core_Root%\corerun "%R2RVALIDATE_DIR%\r2rvalidate.dll" --in main_bubble.dll --ref %Core_Root% --ref . --expect-manifest-ref assemblyC --expect-manifest-ref assemblyB + set ValStatus=!ERRORLEVEL! + IF NOT !ValStatus!==100 ( + ECHO R2R validation failed with exitcode - !ValStatus! + Exit /b 1 + ) + echo R2R validation passed +) ELSE ( + echo WARNING: r2rvalidate.dll not found, skipping R2R validation +) + endlocal ]]> diff --git a/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj b/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj index a0d01bb3168d72..a619df32931c37 100644 --- a/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj +++ b/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj @@ -92,6 +92,21 @@ if not exist main_crossmodule.map ( exit /b 1 ) +REM R2R Validation: verify cross-module inlining artifacts and async thunks +set R2RVALIDATE_DIR=..\..\r2rvalidate\r2rvalidate +if exist "%R2RVALIDATE_DIR%\r2rvalidate.dll" ( + echo Running R2R validation on main_crossmodule.dll... + %Core_Root%\corerun "%R2RVALIDATE_DIR%\r2rvalidate.dll" --in main_crossmodule.dll --ref %Core_Root% --ref . --expect-manifest-ref assemblyC --expect-manifest-ref assemblyB --expect-inlined TestTypeRef_CrossModuleOwn --expect-inlined TestTypeRef_Transitive --expect-inlined TestMethodCall_Transitive --expect-inlined TestFieldAccess_Transitive --expect-inlined TestGeneric_MixedOrigin --expect-inlined TestGeneric_CrossModuleDefinition --expect-inlined TestNestedType_External --expect-inlined TestTypeForwarder --expect-async-thunks TestAsyncTypeRef_CrossModuleOwn --expect-async-thunks TestAsyncTypeRef_Transitive --expect-async-thunks TestAsyncMethodCall_Transitive --expect-async-thunks TestAsyncFieldAccess_Transitive --expect-async-thunks TestAsyncGeneric_MixedOrigin --expect-async-thunks TestAsyncVoid_CrossModuleOwn --expect-async-thunks TestAsyncVoid_Transitive + set ValStatus=!ERRORLEVEL! + IF NOT !ValStatus!==100 ( + ECHO R2R validation failed with exitcode - !ValStatus! + Exit /b 1 + ) + echo R2R validation passed +) ELSE ( + echo WARNING: r2rvalidate.dll not found, skipping R2R validation +) + endlocal ]]> diff --git a/src/tests/readytorun/crossmoduleresolution/r2rvalidate/R2RValidate.cs b/src/tests/readytorun/crossmoduleresolution/r2rvalidate/R2RValidate.cs new file mode 100644 index 00000000000000..6791f99d6639c1 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/r2rvalidate/R2RValidate.cs @@ -0,0 +1,303 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Runtime.CompilerServices; +using ILCompiler.Reflection.ReadyToRun; +using Internal.ReadyToRunConstants; + +/// +/// Validates R2R images for expected cross-module inlining artifacts. +/// Usage: R2RValidate --in <r2r.dll> --ref <dir> +/// [--expect-manifest-ref <assemblyName>]... +/// [--expect-inlined <methodSubstring>]... +/// [--expect-async-thunks <methodSubstring>]... +/// [--expect-no-inlining <methodSubstring>]... +/// +class R2RValidate +{ + static int Main(string[] args) + { + string inputFile = null; + var refPaths = new List(); + var expectedManifestRefs = new List(); + var expectedInlined = new List(); + var expectedAsyncThunks = new List(); + var expectedNoInlining = new List(); + + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--in": + inputFile = args[++i]; + break; + case "--ref": + refPaths.Add(args[++i]); + break; + case "--expect-manifest-ref": + expectedManifestRefs.Add(args[++i]); + break; + case "--expect-inlined": + expectedInlined.Add(args[++i]); + break; + case "--expect-async-thunks": + expectedAsyncThunks.Add(args[++i]); + break; + case "--expect-no-inlining": + expectedNoInlining.Add(args[++i]); + break; + default: + Console.Error.WriteLine($"Unknown argument: {args[i]}"); + return 1; + } + } + + if (inputFile is null) + { + Console.Error.WriteLine("Usage: R2RValidate --in --ref [options]"); + return 1; + } + + try + { + return Validate(inputFile, refPaths, expectedManifestRefs, expectedInlined, expectedAsyncThunks, expectedNoInlining); + } + catch (Exception ex) + { + Console.Error.WriteLine($"FAIL: {ex.Message}"); + Console.Error.WriteLine(ex.StackTrace); + return 1; + } + } + + static int Validate( + string inputFile, + List refPaths, + List expectedManifestRefs, + List expectedInlined, + List expectedAsyncThunks, + List expectedNoInlining) + { + var resolver = new SimpleAssemblyResolver(refPaths); + var reader = new ReadyToRunReader(resolver, inputFile); + + Console.WriteLine($"R2R Image: {inputFile}"); + Console.WriteLine($" Composite: {reader.Composite}"); + + // Collect all assembly references (MSIL + manifest metadata) + var allAssemblyRefs = new Dictionary(); + + if (!reader.Composite) + { + var globalReader = reader.GetGlobalMetadata().MetadataReader; + int msilRefCount = globalReader.GetTableRowCount(TableIndex.AssemblyRef); + for (int i = 1; i <= msilRefCount; i++) + { + var asmRef = globalReader.GetAssemblyReference(MetadataTokens.AssemblyReferenceHandle(i)); + string name = globalReader.GetString(asmRef.Name); + allAssemblyRefs[name] = i; + } + } + foreach (var kvp in reader.ManifestReferenceAssemblies) + allAssemblyRefs[kvp.Key] = kvp.Value; + + Console.WriteLine($" Assembly references count: {allAssemblyRefs.Count}"); + foreach (var kvp in allAssemblyRefs.OrderBy(k => k.Value)) + Console.WriteLine($" [{kvp.Value}] {kvp.Key}"); + + var methods = reader.Methods.ToList(); + Console.WriteLine($" R2R Methods count: {methods.Count}"); + + bool allPassed = true; + + // 1. Validate assembly references (MSIL + manifest) + foreach (string expected in expectedManifestRefs) + { + if (allAssemblyRefs.ContainsKey(expected)) + { + Console.WriteLine($" PASS: AssemblyRef '{expected}' found (index {allAssemblyRefs[expected]})"); + } + else + { + Console.Error.WriteLine($" FAIL: AssemblyRef '{expected}' NOT found. Available: [{string.Join(", ", allAssemblyRefs.Keys)}]"); + allPassed = false; + } + } + + // Build method lookup + // (methods already populated above for diagnostics) + + // 2. Validate CHECK_IL_BODY fixups (proof of cross-module inlining) + foreach (string pattern in expectedInlined) + { + var matching = methods.Where(m => MethodMatches(m, pattern)).ToList(); + if (matching.Count == 0) + { + Console.Error.WriteLine($" FAIL: No R2R method matching '{pattern}' found"); + allPassed = false; + continue; + } + + foreach (var method in matching) + { + bool hasCheckILBody = method.Fixups != null && + method.Fixups.Any(f => f.Signature?.FixupKind == ReadyToRunFixupKind.Check_IL_Body || + f.Signature?.FixupKind == ReadyToRunFixupKind.Verify_IL_Body); + + if (hasCheckILBody) + { + Console.WriteLine($" PASS: '{method.SignatureString}' has CHECK_IL_BODY fixup (cross-module inlining confirmed)"); + } + else + { + string fixupKinds = method.Fixups is null ? "none" : + string.Join(", ", method.Fixups.Select(f => f.Signature?.FixupKind.ToString() ?? "null")); + Console.Error.WriteLine($" FAIL: '{method.SignatureString}' has NO CHECK_IL_BODY fixup. Fixups: [{fixupKinds}]"); + allPassed = false; + } + } + } + + // 3. Validate async thunks (3+ RuntimeFunctions per method) + foreach (string pattern in expectedAsyncThunks) + { + var matching = methods.Where(m => MethodMatches(m, pattern)).ToList(); + if (matching.Count == 0) + { + Console.Error.WriteLine($" FAIL: No R2R method matching '{pattern}' found for async thunk check"); + allPassed = false; + continue; + } + + foreach (var method in matching) + { + int rtfCount = method.RuntimeFunctions.Count; + if (rtfCount >= 3) + { + Console.WriteLine($" PASS: '{method.SignatureString}' has {rtfCount} RuntimeFunctions (async thunk confirmed)"); + } + else + { + Console.Error.WriteLine($" FAIL: '{method.SignatureString}' has only {rtfCount} RuntimeFunction(s), expected >= 3 for async thunk"); + allPassed = false; + } + } + } + + // 4. Validate methods that should NOT have cross-module inlining + foreach (string pattern in expectedNoInlining) + { + var matching = methods.Where(m => MethodMatches(m, pattern)).ToList(); + if (matching.Count == 0) + { + // Method not in R2R at all — that's fine for this check + Console.WriteLine($" PASS: '{pattern}' not in R2R (no inlining, as expected)"); + continue; + } + + foreach (var method in matching) + { + bool hasCheckILBody = method.Fixups != null && + method.Fixups.Any(f => f.Signature?.FixupKind == ReadyToRunFixupKind.Check_IL_Body || + f.Signature?.FixupKind == ReadyToRunFixupKind.Verify_IL_Body); + + if (!hasCheckILBody) + { + Console.WriteLine($" PASS: '{method.SignatureString}' has no CHECK_IL_BODY fixup (no cross-module inlining, as expected)"); + } + else + { + Console.Error.WriteLine($" FAIL: '{method.SignatureString}' unexpectedly has CHECK_IL_BODY fixup"); + allPassed = false; + } + } + } + + // Summary + Console.WriteLine(); + if (allPassed) + { + Console.WriteLine($"R2R VALIDATION PASSED: {inputFile}"); + return 100; + } + else + { + Console.Error.WriteLine($"R2R VALIDATION FAILED: {inputFile}"); + return 1; + } + } + + static bool MethodMatches(ReadyToRunMethod method, string pattern) + { + string sig = method.SignatureString; + if (sig is null) + return false; + // Skip [ASYNC] and [RESUME] sub-entries — only match primary method entries + if (sig.StartsWith("[ASYNC]") || sig.StartsWith("[RESUME]")) + return false; + return sig.Contains(pattern, StringComparison.OrdinalIgnoreCase); + } +} + +/// +/// Simple assembly resolver that probes reference directories. +/// +class SimpleAssemblyResolver : IAssemblyResolver +{ + private static readonly string[] s_probeExtensions = { ".ni.exe", ".ni.dll", ".exe", ".dll" }; + private readonly List _refPaths; + + public SimpleAssemblyResolver(List refPaths) + { + _refPaths = refPaths; + } + + public IAssemblyMetadata FindAssembly(MetadataReader metadataReader, AssemblyReferenceHandle assemblyReferenceHandle, string parentFile) + { + string simpleName = metadataReader.GetString(metadataReader.GetAssemblyReference(assemblyReferenceHandle).Name); + return FindAssembly(simpleName, parentFile); + } + + public IAssemblyMetadata FindAssembly(string simpleName, string parentFile) + { + var allPaths = new List { Path.GetDirectoryName(parentFile) }; + allPaths.AddRange(_refPaths); + + foreach (string refPath in allPaths) + { + foreach (string ext in s_probeExtensions) + { + string probeFile = Path.Combine(refPath, simpleName + ext); + if (File.Exists(probeFile)) + { + try + { + return Open(probeFile); + } + catch (BadImageFormatException) + { + } + } + } + } + + return null; + } + + private static IAssemblyMetadata Open(string filename) + { + byte[] image = File.ReadAllBytes(filename); + PEReader peReader = new(Unsafe.As>(ref image)); + if (!peReader.HasMetadata) + throw new BadImageFormatException($"ECMA metadata not found in file '{filename}'"); + return new StandaloneAssemblyMetadata(peReader); + } +} diff --git a/src/tests/readytorun/crossmoduleresolution/r2rvalidate/r2rvalidate.csproj b/src/tests/readytorun/crossmoduleresolution/r2rvalidate/r2rvalidate.csproj new file mode 100644 index 00000000000000..161a29e63dd237 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/r2rvalidate/r2rvalidate.csproj @@ -0,0 +1,14 @@ + + + Exe + true + BuildOnly + 1 + + + + + + + + From 1575e66b3926599b8f96ac8a50295235fd96e7a1 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:11:08 -0700 Subject: [PATCH 07/74] Extract crossgen2 precommands into shared .targets file Replace ~160 lines of inline bash/batch in each csproj with a shared crossmoduleresolution.targets file that generates scripts from MSBuild items and properties. Each csproj now declaratively specifies: - Crossgen2Step items (dependency assemblies with extra args) - Crossgen2MainRef items (references for main assembly) - R2RExpect* items (validation expectations) - Crossgen2CommonArgs/MainExtraArgs properties The targets file generates: - A __crossgen2() helper function (bash) / :__cg2_invoke subroutine (batch) - IL_DLLS copy loop from Crossgen2Step + main assembly names - Per-step crossgen2 calls via @() item transforms - Main assembly compilation with extra args and --map - R2R validation invocation with args from R2RExpect* items Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../crossmoduleresolution.targets | 160 +++++++++++++++ .../main/main_bubble.csproj | 189 ++--------------- .../main/main_crossmodule.csproj | 192 +++--------------- 3 files changed, 204 insertions(+), 337 deletions(-) create mode 100644 src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets diff --git a/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets b/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets new file mode 100644 index 00000000000000..b05eb7c29dfd9c --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets @@ -0,0 +1,160 @@ + + + + + <_R2RValArgs>@(R2RExpectRef->'--expect-manifest-ref %(Identity)', ' ') + <_R2RValArgs>$(_R2RValArgs) @(R2RExpectInlined->'--expect-inlined %(Identity)', ' ') + <_R2RValArgs>$(_R2RValArgs) @(R2RExpectAsyncThunks->'--expect-async-thunks %(Identity)', ' ') + + nul + +REM Copy IL assemblies before crossgen2 overwrites them +for %%A in (@(Crossgen2Step->'%(Identity)', ' ') $(Crossgen2MainAssembly)) do ( + if not exist IL_DLLS\%%A.dll ( + copy /y %%A.dll IL_DLLS\%%A.dll + if not exist IL_DLLS\%%A.dll ( + echo FAILED to copy %%A.dll to IL_DLLS + exit /b 1 + ) + ) +) + +REM Crossgen2 each dependency assembly +@(Crossgen2Step->'call :__cg2_invoke %(Identity) %(ExtraArgs)%0aIF NOT !ERRORLEVEL!==0 Exit /b 1', '%0a') + +REM Crossgen2 main assembly +call :__cg2_invoke $(Crossgen2MainAssembly) @(Crossgen2MainRef->'-r:%(Identity).dll', ' ') $(Crossgen2MainExtraArgs) --map +IF NOT !ERRORLEVEL!==0 Exit /b 1 + +if not exist $(Crossgen2MainAssembly).map ( + echo FAILED to build $(Crossgen2MainAssembly).dll - no map file + exit /b 1 +) + +REM R2R Validation +set R2RVALIDATE_DIR=..\..\r2rvalidate\r2rvalidate +if exist "%R2RVALIDATE_DIR%\r2rvalidate.dll" ( + echo Running R2R validation on $(Crossgen2MainAssembly).dll... + %Core_Root%\corerun "%R2RVALIDATE_DIR%\r2rvalidate.dll" --in $(Crossgen2MainAssembly).dll --ref %Core_Root% --ref . $(_R2RValArgs) + set ValStatus=!ERRORLEVEL! + IF NOT !ValStatus!==100 ( + ECHO R2R validation failed with exitcode - !ValStatus! + Exit /b 1 + ) + echo R2R validation passed +) ELSE ( + echo WARNING: r2rvalidate.dll not found, skipping R2R validation +) + +endlocal +]]> + + '%(Identity)', ' ') $(Crossgen2MainAssembly)%3B do + if [ ! -f "IL_DLLS/${asm}.dll" ]%3B then + cp "${asm}.dll" "IL_DLLS/${asm}.dll" + if [ ! -f "IL_DLLS/${asm}.dll" ]%3B then + echo "FAILED to copy ${asm}.dll to IL_DLLS" + exit 1 + fi + fi +done + +# Crossgen2 each dependency assembly +@(Crossgen2Step->'__crossgen2 %(Identity) %(ExtraArgs)', '%0a') + +# Crossgen2 main assembly +__crossgen2 $(Crossgen2MainAssembly) @(Crossgen2MainRef->'-r:%(Identity).dll', ' ') $(Crossgen2MainExtraArgs) --map + +if [ ! -f $(Crossgen2MainAssembly).map ]%3B then + echo "FAILED to build $(Crossgen2MainAssembly).dll - no map file" + exit 1 +fi + +# R2R Validation +R2RVALIDATE_DIR="../../r2rvalidate/r2rvalidate" +if [ -f "${R2RVALIDATE_DIR}/r2rvalidate.dll" ]%3B then + echo "Running R2R validation on $(Crossgen2MainAssembly).dll..." + "${CORE_ROOT}"/corerun "${R2RVALIDATE_DIR}/r2rvalidate.dll" --in $(Crossgen2MainAssembly).dll --ref "${CORE_ROOT}" --ref . $(_R2RValArgs) + __valExitCode=$? + if [ $__valExitCode -ne 100 ]%3B then + echo "R2R validation failed with exitcode: $__valExitCode" + exit 1 + fi + echo "R2R validation passed" +else + echo "WARNING: r2rvalidate.dll not found, skipping R2R validation" +fi + +export DOTNET_GCName DOTNET_GCStress DOTNET_HeapVerify DOTNET_ReadyToRun +]]> + + diff --git a/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj b/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj index a0676ac4963d6e..8498e89b5b199b 100644 --- a/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj +++ b/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj @@ -9,7 +9,13 @@ $(NoWarn);CS1998 true + + + --opt-async-methods + main_bubble + --inputbubbleref:assemblyB.dll + @@ -19,175 +25,22 @@ - - nul - -REM Copy IL assemblies before crossgen2 overwrites them -for %%A in (main_bubble assemblyB assemblyC assemblyD assemblyE) do ( - if not exist IL_DLLS\%%A.dll ( - copy /y %%A.dll IL_DLLS\%%A.dll - if not exist IL_DLLS\%%A.dll ( - echo FAILED to copy %%A.dll to IL_DLLS - exit /b 1 - ) - ) -) - -REM Crossgen2 assemblyD (no special flags) -%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -o:assemblyD.dll IL_DLLS\assemblyD.dll - -set CrossGenStatus=!ERRORLEVEL! -IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen assemblyD failed with exitcode - !CrossGenStatus! - Exit /b 1 -) - -REM Crossgen2 assemblyE (references assemblyD) -%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS\assemblyE.dll - -set CrossGenStatus=!ERRORLEVEL! -IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen assemblyE failed with exitcode - !CrossGenStatus! - Exit /b 1 -) - -REM Crossgen2 assemblyB (no special flags) -%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -o:assemblyB.dll IL_DLLS\assemblyB.dll - -set CrossGenStatus=!ERRORLEVEL! -IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen assemblyB failed with exitcode - !CrossGenStatus! - Exit /b 1 -) - -REM Crossgen2 assemblyC (references assemblyD, assemblyE) -%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS\assemblyC.dll - -set CrossGenStatus=!ERRORLEVEL! -IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen assemblyC failed with exitcode - !CrossGenStatus! - Exit /b 1 -) - -REM Crossgen2 main with --inputbubbleref assemblyB (version bubble) -%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods --map -r:%Core_Root%\*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --inputbubbleref:assemblyB.dll -o:main_bubble.dll IL_DLLS\main_bubble.dll - -set CrossGenStatus=!ERRORLEVEL! -IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen main_bubble failed with exitcode - !CrossGenStatus! - Exit /b 1 -) -if not exist main_bubble.map ( - echo FAILED to build main_bubble.dll - no map file - exit /b 1 -) -REM R2R Validation: verify assembly references present (no cross-module inlining expected) -set R2RVALIDATE_DIR=..\..\r2rvalidate\r2rvalidate -if exist "%R2RVALIDATE_DIR%\r2rvalidate.dll" ( - echo Running R2R validation on main_bubble.dll... - %Core_Root%\corerun "%R2RVALIDATE_DIR%\r2rvalidate.dll" --in main_bubble.dll --ref %Core_Root% --ref . --expect-manifest-ref assemblyC --expect-manifest-ref assemblyB - set ValStatus=!ERRORLEVEL! - IF NOT !ValStatus!==100 ( - ECHO R2R validation failed with exitcode - !ValStatus! - Exit /b 1 - ) - echo R2R validation passed -) ELSE ( - echo WARNING: r2rvalidate.dll not found, skipping R2R validation -) - -endlocal -]]> - + + + + + + + + + -# R2R Validation: verify assembly references present (no cross-module inlining expected) -R2RVALIDATE_DIR="../../r2rvalidate/r2rvalidate" -if [ -f "${R2RVALIDATE_DIR}/r2rvalidate.dll" ]; then - echo "Running R2R validation on main_bubble.dll..." - "${CORE_ROOT}"/corerun "${R2RVALIDATE_DIR}/r2rvalidate.dll" --in main_bubble.dll --ref "${CORE_ROOT}" --ref . --expect-manifest-ref assemblyC --expect-manifest-ref assemblyB - __valExitCode=$? - if [ $__valExitCode -ne 100 ]; then - echo "R2R validation failed with exitcode: $__valExitCode" - exit 1 - fi - echo "R2R validation passed" -else - echo "WARNING: r2rvalidate.dll not found at ${R2RVALIDATE_DIR}, skipping R2R validation" -fi + + + + -export DOTNET_GCName DOTNET_GCStress DOTNET_HeapVerify DOTNET_ReadyToRun -]]> - + diff --git a/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj b/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj index a619df32931c37..5c94f66283c906 100644 --- a/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj +++ b/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj @@ -9,7 +9,13 @@ $(NoWarn);CS1998 true + + + --opt-async-methods + main_crossmodule + --opt-cross-module:assemblyC + @@ -19,176 +25,24 @@ - - nul - -REM Copy IL assemblies before crossgen2 overwrites them -for %%A in (main_crossmodule assemblyB assemblyC assemblyD assemblyE) do ( - if not exist IL_DLLS\%%A.dll ( - copy /y %%A.dll IL_DLLS\%%A.dll - if not exist IL_DLLS\%%A.dll ( - echo FAILED to copy %%A.dll to IL_DLLS - exit /b 1 - ) - ) -) - -REM Crossgen2 assemblyD (no special flags) -%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -o:assemblyD.dll IL_DLLS\assemblyD.dll - -set CrossGenStatus=!ERRORLEVEL! -IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen assemblyD failed with exitcode - !CrossGenStatus! - Exit /b 1 -) - -REM Crossgen2 assemblyE (references assemblyD) -%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -r:assemblyD.dll -o:assemblyE.dll IL_DLLS\assemblyE.dll - -set CrossGenStatus=!ERRORLEVEL! -IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen assemblyE failed with exitcode - !CrossGenStatus! - Exit /b 1 -) - -REM Crossgen2 assemblyB (no special flags) -%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -o:assemblyB.dll IL_DLLS\assemblyB.dll - -set CrossGenStatus=!ERRORLEVEL! -IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen assemblyB failed with exitcode - !CrossGenStatus! - Exit /b 1 -) - -REM Crossgen2 assemblyC (references assemblyD, assemblyE) -%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods -r:%Core_Root%\*.dll -r:assemblyD.dll -r:assemblyE.dll -o:assemblyC.dll IL_DLLS\assemblyC.dll - -set CrossGenStatus=!ERRORLEVEL! -IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen assemblyC failed with exitcode - !CrossGenStatus! - Exit /b 1 -) - -REM Crossgen2 main with --opt-cross-module:assemblyC -%Core_Root%\crossgen2\crossgen2.exe --opt-async-methods --map -r:%Core_Root%\*.dll -r:assemblyB.dll -r:assemblyC.dll -r:assemblyD.dll -r:assemblyE.dll --opt-cross-module:assemblyC -o:main_crossmodule.dll IL_DLLS\main_crossmodule.dll - -set CrossGenStatus=!ERRORLEVEL! -IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen main_crossmodule failed with exitcode - !CrossGenStatus! - Exit /b 1 -) -if not exist main_crossmodule.map ( - echo FAILED to build main_crossmodule.dll - no map file - exit /b 1 -) -REM R2R Validation: verify cross-module inlining artifacts and async thunks -set R2RVALIDATE_DIR=..\..\r2rvalidate\r2rvalidate -if exist "%R2RVALIDATE_DIR%\r2rvalidate.dll" ( - echo Running R2R validation on main_crossmodule.dll... - %Core_Root%\corerun "%R2RVALIDATE_DIR%\r2rvalidate.dll" --in main_crossmodule.dll --ref %Core_Root% --ref . --expect-manifest-ref assemblyC --expect-manifest-ref assemblyB --expect-inlined TestTypeRef_CrossModuleOwn --expect-inlined TestTypeRef_Transitive --expect-inlined TestMethodCall_Transitive --expect-inlined TestFieldAccess_Transitive --expect-inlined TestGeneric_MixedOrigin --expect-inlined TestGeneric_CrossModuleDefinition --expect-inlined TestNestedType_External --expect-inlined TestTypeForwarder --expect-async-thunks TestAsyncTypeRef_CrossModuleOwn --expect-async-thunks TestAsyncTypeRef_Transitive --expect-async-thunks TestAsyncMethodCall_Transitive --expect-async-thunks TestAsyncFieldAccess_Transitive --expect-async-thunks TestAsyncGeneric_MixedOrigin --expect-async-thunks TestAsyncVoid_CrossModuleOwn --expect-async-thunks TestAsyncVoid_Transitive - set ValStatus=!ERRORLEVEL! - IF NOT !ValStatus!==100 ( - ECHO R2R validation failed with exitcode - !ValStatus! - Exit /b 1 - ) - echo R2R validation passed -) ELSE ( - echo WARNING: r2rvalidate.dll not found, skipping R2R validation -) - -endlocal -]]> - + + + + + + + + + -# R2R Validation: verify cross-module inlining artifacts and async thunks -# Path is relative to this test's output directory (script cd's here before running) -R2RVALIDATE_DIR="../../r2rvalidate/r2rvalidate" -if [ -f "${R2RVALIDATE_DIR}/r2rvalidate.dll" ]; then - echo "Running R2R validation on main_crossmodule.dll..." - "${CORE_ROOT}"/corerun "${R2RVALIDATE_DIR}/r2rvalidate.dll" --in main_crossmodule.dll --ref "${CORE_ROOT}" --ref . --expect-manifest-ref assemblyC --expect-manifest-ref assemblyB --expect-inlined TestTypeRef_CrossModuleOwn --expect-inlined TestTypeRef_Transitive --expect-inlined TestMethodCall_Transitive --expect-inlined TestFieldAccess_Transitive --expect-inlined TestGeneric_MixedOrigin --expect-inlined TestGeneric_CrossModuleDefinition --expect-inlined TestNestedType_External --expect-inlined TestTypeForwarder --expect-async-thunks TestAsyncTypeRef_CrossModuleOwn --expect-async-thunks TestAsyncTypeRef_Transitive --expect-async-thunks TestAsyncMethodCall_Transitive --expect-async-thunks TestAsyncFieldAccess_Transitive --expect-async-thunks TestAsyncGeneric_MixedOrigin --expect-async-thunks TestAsyncVoid_CrossModuleOwn --expect-async-thunks TestAsyncVoid_Transitive - __valExitCode=$? - if [ $__valExitCode -ne 100 ]; then - echo "R2R validation failed with exitcode: $__valExitCode" - exit 1 - fi - echo "R2R validation passed" -else - echo "WARNING: r2rvalidate.dll not found at ${R2RVALIDATE_DIR}, skipping R2R validation" -fi + + + + + + -export DOTNET_GCName DOTNET_GCStress DOTNET_HeapVerify DOTNET_ReadyToRun -]]> - + From ee260403d2dd09c85518ba83bf934d80da0f9846 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:31:08 -0700 Subject: [PATCH 08/74] Refactor crossgen2 orchestration into C# console app Replace inline bash/batch precommands with a C# RunCrossgen tool invoked at MSBuild build time via corerun. The targets file now uses an AfterTargets=Build Exec target that runs runcrossgen.dll to: - Copy IL assemblies before crossgen2 overwrites them - Run crossgen2 on each dependency in declared order - Run crossgen2 on the main assembly with refs and flags - Run r2rvalidate for R2R image validation This moves all crossgen2 work from test execution time to build time, making the generated test scripts clean (no precommands). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../crossmoduleresolution.targets | 209 +++++------------ .../runcrossgen/RunCrossgen.cs | 210 ++++++++++++++++++ .../runcrossgen/runcrossgen.csproj | 10 + 3 files changed, 281 insertions(+), 148 deletions(-) create mode 100644 src/tests/readytorun/crossmoduleresolution/runcrossgen/RunCrossgen.cs create mode 100644 src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj diff --git a/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets b/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets index b05eb7c29dfd9c..c0041567ed46cd 100644 --- a/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets +++ b/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets @@ -1,160 +1,73 @@ - - - <_R2RValArgs>@(R2RExpectRef->'--expect-manifest-ref %(Identity)', ' ') - <_R2RValArgs>$(_R2RValArgs) @(R2RExpectInlined->'--expect-inlined %(Identity)', ' ') - <_R2RValArgs>$(_R2RValArgs) @(R2RExpectAsyncThunks->'--expect-async-thunks %(Identity)', ' ') - - nul - -REM Copy IL assemblies before crossgen2 overwrites them -for %%A in (@(Crossgen2Step->'%(Identity)', ' ') $(Crossgen2MainAssembly)) do ( - if not exist IL_DLLS\%%A.dll ( - copy /y %%A.dll IL_DLLS\%%A.dll - if not exist IL_DLLS\%%A.dll ( - echo FAILED to copy %%A.dll to IL_DLLS - exit /b 1 - ) - ) -) - -REM Crossgen2 each dependency assembly -@(Crossgen2Step->'call :__cg2_invoke %(Identity) %(ExtraArgs)%0aIF NOT !ERRORLEVEL!==0 Exit /b 1', '%0a') - -REM Crossgen2 main assembly -call :__cg2_invoke $(Crossgen2MainAssembly) @(Crossgen2MainRef->'-r:%(Identity).dll', ' ') $(Crossgen2MainExtraArgs) --map -IF NOT !ERRORLEVEL!==0 Exit /b 1 - -if not exist $(Crossgen2MainAssembly).map ( - echo FAILED to build $(Crossgen2MainAssembly).dll - no map file - exit /b 1 -) - -REM R2R Validation -set R2RVALIDATE_DIR=..\..\r2rvalidate\r2rvalidate -if exist "%R2RVALIDATE_DIR%\r2rvalidate.dll" ( - echo Running R2R validation on $(Crossgen2MainAssembly).dll... - %Core_Root%\corerun "%R2RVALIDATE_DIR%\r2rvalidate.dll" --in $(Crossgen2MainAssembly).dll --ref %Core_Root% --ref . $(_R2RValArgs) - set ValStatus=!ERRORLEVEL! - IF NOT !ValStatus!==100 ( - ECHO R2R validation failed with exitcode - !ValStatus! - Exit /b 1 - ) - echo R2R validation passed -) ELSE ( - echo WARNING: r2rvalidate.dll not found, skipping R2R validation -) - -endlocal -]]> - - '%(Identity)', ' ') $(Crossgen2MainAssembly)%3B do - if [ ! -f "IL_DLLS/${asm}.dll" ]%3B then - cp "${asm}.dll" "IL_DLLS/${asm}.dll" - if [ ! -f "IL_DLLS/${asm}.dll" ]%3B then - echo "FAILED to copy ${asm}.dll to IL_DLLS" - exit 1 - fi - fi -done - -# Crossgen2 each dependency assembly -@(Crossgen2Step->'__crossgen2 %(Identity) %(ExtraArgs)', '%0a') - -# Crossgen2 main assembly -__crossgen2 $(Crossgen2MainAssembly) @(Crossgen2MainRef->'-r:%(Identity).dll', ' ') $(Crossgen2MainExtraArgs) --map - -if [ ! -f $(Crossgen2MainAssembly).map ]%3B then - echo "FAILED to build $(Crossgen2MainAssembly).dll - no map file" - exit 1 -fi - -# R2R Validation -R2RVALIDATE_DIR="../../r2rvalidate/r2rvalidate" -if [ -f "${R2RVALIDATE_DIR}/r2rvalidate.dll" ]%3B then - echo "Running R2R validation on $(Crossgen2MainAssembly).dll..." - "${CORE_ROOT}"/corerun "${R2RVALIDATE_DIR}/r2rvalidate.dll" --in $(Crossgen2MainAssembly).dll --ref "${CORE_ROOT}" --ref . $(_R2RValArgs) - __valExitCode=$? - if [ $__valExitCode -ne 100 ]%3B then - echo "R2R validation failed with exitcode: $__valExitCode" - exit 1 - fi - echo "R2R validation passed" -else - echo "WARNING: r2rvalidate.dll not found, skipping R2R validation" -fi - -export DOTNET_GCName DOTNET_GCStress DOTNET_HeapVerify DOTNET_ReadyToRun -]]> + + <_RunCrossgenProjectDir>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', 'runcrossgen')) + <_R2RValidateProjectDir>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', 'r2rvalidate')) + + + + + + <_Crossgen2Path>$(CORE_ROOT)/crossgen2/crossgen2 + <_CoreRunPath>$(CORE_ROOT)/corerun + + + + + <_StepArgs>@(Crossgen2Step->'--assembly %(Identity) --extra-args "%(ExtraArgs)"', ' ') + <_MainRefList>@(Crossgen2MainRef->'%(Identity)', ',') + + + + + <_R2RValArgs>@(R2RExpectRef->'--expect-manifest-ref %(Identity)', ' ') + <_R2RValArgs>$(_R2RValArgs) @(R2RExpectInlined->'--expect-inlined %(Identity)', ' ') + <_R2RValArgs>$(_R2RValArgs) @(R2RExpectAsyncThunks->'--expect-async-thunks %(Identity)', ' ') + + + + + <_ValidateDll>$([MSBuild]::NormalizePath('$(OutputPath)', '..', '..', 'r2rvalidate', 'r2rvalidate', 'r2rvalidate.dll')) + + + + + <_RunCrossgenDll>$([MSBuild]::NormalizePath('$(OutputPath)', '..', '..', 'runcrossgen', 'runcrossgen', 'runcrossgen.dll')) + + + + + + + diff --git a/src/tests/readytorun/crossmoduleresolution/runcrossgen/RunCrossgen.cs b/src/tests/readytorun/crossmoduleresolution/runcrossgen/RunCrossgen.cs new file mode 100644 index 00000000000000..4b4a209c60c864 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/runcrossgen/RunCrossgen.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +/// +/// Orchestrates crossgen2 compilation and R2R validation for cross-module resolution tests. +/// +/// Usage: +/// RunCrossgen --crossgen2 <path> --output-dir <dir> --ref-dir <dir> +/// --assembly <name> [--extra-args <args>] (repeatable, order matters) +/// --main <name> --main-extra-args <args> --main-refs <name,...> +/// [--common-args <args>] +/// [--validate <r2rvalidate.dll path> --validate-args <args>] +/// +class RunCrossgen +{ + static int Main(string[] args) + { + string crossgen2Path = null; + string outputDir = null; + string refDir = null; + string coreRunPath = null; + string commonArgs = ""; + string mainAssembly = null; + string mainExtraArgs = ""; + string mainRefs = ""; + string validatePath = null; + string validateArgs = ""; + + var steps = new List<(string name, string extraArgs)>(); + + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--crossgen2": + crossgen2Path = args[++i]; + break; + case "--output-dir": + outputDir = args[++i]; + break; + case "--ref-dir": + refDir = args[++i]; + break; + case "--corerun": + coreRunPath = args[++i]; + break; + case "--common-args": + commonArgs = args[++i]; + break; + case "--assembly": + string name = args[++i]; + string extra = ""; + if (i + 1 < args.Length && args[i + 1] == "--extra-args") + { + i++; + extra = args[++i]; + } + steps.Add((name, extra)); + break; + case "--main": + mainAssembly = args[++i]; + break; + case "--main-extra-args": + mainExtraArgs = args[++i]; + break; + case "--main-refs": + mainRefs = args[++i]; + break; + case "--validate": + validatePath = args[++i]; + break; + case "--validate-args": + validateArgs = args[++i]; + break; + default: + Console.Error.WriteLine($"Unknown argument: {args[i]}"); + return 1; + } + } + + if (crossgen2Path is null || outputDir is null || refDir is null || mainAssembly is null) + { + Console.Error.WriteLine("Required: --crossgen2, --output-dir, --ref-dir, --main"); + return 1; + } + + // All assembly names (deps + main) + var allAssemblies = steps.Select(s => s.name).Append(mainAssembly).ToList(); + + // 1. Copy IL assemblies to IL_DLLS/ before crossgen2 overwrites them + string ilDir = Path.Combine(outputDir, "IL_DLLS"); + Directory.CreateDirectory(ilDir); + + foreach (string asm in allAssemblies) + { + string src = Path.Combine(outputDir, $"{asm}.dll"); + string dst = Path.Combine(ilDir, $"{asm}.dll"); + if (!File.Exists(dst)) + { + if (!File.Exists(src)) + { + Console.Error.WriteLine($"FAILED: source assembly not found: {src}"); + return 1; + } + File.Copy(src, dst); + Console.WriteLine($"Copied {asm}.dll to IL_DLLS/"); + } + } + + // 2. Crossgen2 each dependency assembly + foreach (var (name, extraArgs) in steps) + { + int exitCode = RunCrossgen2(crossgen2Path, outputDir, refDir, commonArgs, + name, extraArgs); + if (exitCode != 0) + return exitCode; + } + + // 3. Crossgen2 main assembly with refs and extra args + string refArgs = string.Join(" ", mainRefs.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(r => $"-r:{r.Trim()}.dll")); + string allMainExtra = $"{refArgs} {mainExtraArgs} --map".Trim(); + + int mainExit = RunCrossgen2(crossgen2Path, outputDir, refDir, commonArgs, + mainAssembly, allMainExtra); + if (mainExit != 0) + return mainExit; + + string mapFile = Path.Combine(outputDir, $"{mainAssembly}.map"); + if (!File.Exists(mapFile)) + { + Console.Error.WriteLine($"FAILED: no map file generated at {mapFile}"); + return 1; + } + + // 4. R2R Validation (optional) + if (validatePath is not null && coreRunPath is not null && File.Exists(validatePath)) + { + Console.WriteLine($"Running R2R validation on {mainAssembly}.dll..."); + string valArgs = $"\"{validatePath}\" --in {mainAssembly}.dll --ref \"{refDir}\" --ref \"{outputDir}\" {validateArgs}"; + + int valExit = RunProcess(coreRunPath, valArgs, outputDir); + if (valExit != 100) + { + Console.Error.WriteLine($"R2R validation failed with exitcode: {valExit}"); + return 1; + } + Console.WriteLine("R2R validation passed"); + } + else if (validatePath is not null) + { + Console.WriteLine($"WARNING: r2rvalidate not found at {validatePath}, skipping"); + } + + Console.WriteLine("Crossgen orchestration completed successfully"); + return 0; + } + + static int RunCrossgen2(string crossgen2Path, string outputDir, string refDir, + string commonArgs, string assemblyName, string extraArgs) + { + string ilDll = Path.Combine("IL_DLLS", $"{assemblyName}.dll"); + string outDll = $"{assemblyName}.dll"; + + // Use glob pattern for reference directory + string refGlob = Path.Combine(refDir, "*.dll"); + string arguments = $"{commonArgs} -r:\"{refGlob}\" {extraArgs} -o:{outDll} {ilDll}".Trim(); + + Console.WriteLine($"Crossgen2 {assemblyName}..."); + int exitCode = RunProcess(crossgen2Path, arguments, outputDir); + + if (exitCode != 0) + { + Console.Error.WriteLine($"Crossgen2 {assemblyName} failed with exitcode: {exitCode}"); + return 1; + } + + return 0; + } + + static int RunProcess(string fileName, string arguments, string workingDir) + { + var psi = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + WorkingDirectory = workingDir, + UseShellExecute = false, + }; + + // Suppress DOTNET variables that interfere with crossgen2 + psi.Environment.Remove("DOTNET_GCName"); + psi.Environment.Remove("DOTNET_GCStress"); + psi.Environment.Remove("DOTNET_HeapVerify"); + psi.Environment.Remove("DOTNET_ReadyToRun"); + + Console.WriteLine($" > {fileName} {arguments}"); + + using var process = Process.Start(psi); + process.WaitForExit(); + + return process.ExitCode; + } +} diff --git a/src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj b/src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj new file mode 100644 index 00000000000000..5d5dabe7c0bcf3 --- /dev/null +++ b/src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj @@ -0,0 +1,10 @@ + + + Exe + BuildOnly + 1 + + + + + From 67ffdd60666c4a34cce6794cb85d7d996546aa2a Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:43:58 -0700 Subject: [PATCH 09/74] Output runcrossgen to CORE_ROOT/runcrossgen/ Set OutputPath to $(CORE_ROOT)/runcrossgen/ so the tool lives alongside crossgen2 and corerun. The targets file now references it from CORE_ROOT instead of navigating relative to the test output directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../crossmoduleresolution/crossmoduleresolution.targets | 4 ++-- .../crossmoduleresolution/runcrossgen/runcrossgen.csproj | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets b/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets index c0041567ed46cd..56e355cb2e868c 100644 --- a/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets +++ b/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets @@ -57,9 +57,9 @@ <_ValidateDll>$([MSBuild]::NormalizePath('$(OutputPath)', '..', '..', 'r2rvalidate', 'r2rvalidate', 'r2rvalidate.dll')) - + - <_RunCrossgenDll>$([MSBuild]::NormalizePath('$(OutputPath)', '..', '..', 'runcrossgen', 'runcrossgen', 'runcrossgen.dll')) + <_RunCrossgenDll>$(CORE_ROOT)/runcrossgen/runcrossgen.dll Exe BuildOnly 1 + $(CORE_ROOT)/runcrossgen + false + false From d5b2d3a39314102e8b7f4b0296ae7e661994919a Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:47:59 -0700 Subject: [PATCH 10/74] Use host SDK RID for runcrossgen build-time tool Set RuntimeIdentifier=$(NETCoreSdkRuntimeIdentifier) since runcrossgen runs at build time on the build machine, not at test time on the target. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../crossmoduleresolution/runcrossgen/runcrossgen.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj b/src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj index 6dfd996aa2ddd7..34ae58fa6638f1 100644 --- a/src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj +++ b/src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj @@ -3,9 +3,12 @@ Exe BuildOnly 1 + $(CORE_ROOT)/runcrossgen false false + + $(NETCoreSdkRuntimeIdentifier) From 80291837f012cf6f2bef65a83f5a8ba1ef9f9bcd Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:19:57 -0700 Subject: [PATCH 11/74] WIP: Add ILCompiler.ReadyToRun.Tests project Roslyn-based test framework for crossgen2 R2R validation: - Compiles test assemblies with Roslyn at test time - Invokes crossgen2 out-of-process via dotnet exec - Validates R2R output using ILCompiler.Reflection.ReadyToRun - 4 test cases: basic inlining, transitive refs, async, composite - Integrated into crossgen2.slnx and clr.toolstests subset Status: crossgen2 invocation works, fixup validation WIP. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Subsets.props | 2 + .../Expectations/R2RExpectationAttributes.cs | 138 +++++++++++ .../ILCompiler.ReadyToRun.Tests.csproj | 56 +++++ .../CrossModuleInlining/AsyncMethods.cs | 27 ++ .../CrossModuleInlining/BasicInlining.cs | 26 ++ .../CrossModuleInlining/CompositeBasic.cs | 20 ++ .../Dependencies/AsyncInlineableLib.cs | 15 ++ .../Dependencies/CompositeLib.cs | 11 + .../Dependencies/ExternalLib.cs | 19 ++ .../Dependencies/InlineableLib.cs | 14 ++ .../Dependencies/InlineableLibTransitive.cs | 14 ++ .../TransitiveReferences.cs | 26 ++ .../TestCases/R2RTestSuites.cs | 136 ++++++++++ .../TestCasesRunner/R2RDriver.cs | 168 +++++++++++++ .../TestCasesRunner/R2RResultChecker.cs | 233 ++++++++++++++++++ .../TestCasesRunner/R2RTestCaseCompiler.cs | 124 ++++++++++ .../TestCasesRunner/R2RTestRunner.cs | 204 +++++++++++++++ .../TestCasesRunner/TestPaths.cs | 183 ++++++++++++++ src/coreclr/tools/aot/crossgen2.slnx | 3 + 19 files changed, 1419 insertions(+) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs diff --git a/eng/Subsets.props b/eng/Subsets.props index 5ce04cb6645861..ef81a5c2c1a8a3 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -502,6 +502,8 @@ Test="true" Category="clr" Condition="'$(DotNetBuildSourceOnly)' != 'true' and '$(NativeAotSupported)' == 'true'"/> + diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs new file mode 100644 index 00000000000000..13a8edf0ad7122 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; + +namespace ILCompiler.ReadyToRun.Tests.Expectations; + +/// +/// Marks a method as expected to be cross-module inlined into the main R2R image. +/// The R2R result checker will verify a CHECK_IL_BODY fixup exists for this method's callee. +/// +[Conditional("R2R_EXPECTATIONS")] +[AttributeUsage(AttributeTargets.Method)] +public sealed class ExpectInlinedAttribute : Attribute +{ + /// + /// The fully qualified name of the method expected to be inlined. + /// If null, infers from the method body (looks for the first cross-module call). + /// + public string? MethodName { get; set; } +} + +/// +/// Marks a method as expected to have async thunk RuntimeFunctions in the R2R image. +/// Async methods compiled with --opt-async-methods produce 3 RuntimeFunctions: +/// thunk + async body + resumption stub. +/// +[Conditional("R2R_EXPECTATIONS")] +[AttributeUsage(AttributeTargets.Method)] +public sealed class ExpectAsyncThunkAttribute : Attribute +{ + /// + /// Expected number of RuntimeFunctions. Defaults to 3 (thunk + body + resumption). + /// + public int ExpectedRuntimeFunctionCount { get; set; } = 3; +} + +/// +/// Declares that the R2R image should contain a manifest reference to the specified assembly. +/// +[Conditional("R2R_EXPECTATIONS")] +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public sealed class ExpectManifestRefAttribute : Attribute +{ + public string AssemblyName { get; } + + public ExpectManifestRefAttribute(string assemblyName) + { + AssemblyName = assemblyName; + } +} + +/// +/// Specifies a crossgen2 command-line option for the main assembly compilation. +/// Applied at the assembly level of the test case. +/// +[Conditional("R2R_EXPECTATIONS")] +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public sealed class Crossgen2OptionAttribute : Attribute +{ + public string Option { get; } + + public Crossgen2OptionAttribute(string option) + { + Option = option; + } +} + +/// +/// Declares a dependency assembly that should be compiled before the main test assembly. +/// The source files are compiled with Roslyn, then optionally crossgen2'd before the main assembly. +/// +[Conditional("R2R_EXPECTATIONS")] +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public sealed class SetupCompileBeforeAttribute : Attribute +{ + /// + /// The output assembly filename (e.g., "InlineableLib.dll"). + /// + public string OutputName { get; } + + /// + /// Source file paths relative to the test case's Dependencies/ folder. + /// + public string[] SourceFiles { get; } + + /// + /// Additional assembly references needed to compile this dependency. + /// + public string[]? References { get; set; } + + /// + /// If true, this assembly is also crossgen2'd before the main assembly. + /// + public bool Crossgen { get; set; } + + /// + /// Additional crossgen2 options for this dependency assembly. + /// + public string[]? CrossgenOptions { get; set; } + + public SetupCompileBeforeAttribute(string outputName, string[] sourceFiles) + { + OutputName = outputName; + SourceFiles = sourceFiles; + } +} + +/// +/// Marks a method as expected to have R2R compiled code in the output image. +/// +[Conditional("R2R_EXPECTATIONS")] +[AttributeUsage(AttributeTargets.Method)] +public sealed class ExpectR2RMethodAttribute : Attribute +{ +} + +/// +/// Marks an assembly-level option to enable composite mode compilation. +/// When present, all SetupCompileBefore assemblies with Crossgen=true are compiled +/// together with the main assembly using --composite. +/// +[Conditional("R2R_EXPECTATIONS")] +[AttributeUsage(AttributeTargets.Assembly)] +public sealed class CompositeModeAttribute : Attribute +{ +} + +/// +/// Marks an assembly-level option to enable runtime-async compilation. +/// Adds Features=runtime-async=on to Roslyn compilation and --opt-async-methods to crossgen2. +/// +[Conditional("R2R_EXPECTATIONS")] +[AttributeUsage(AttributeTargets.Assembly)] +public sealed class EnableRuntimeAsyncAttribute : Attribute +{ +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj new file mode 100644 index 00000000000000..24c1d7f9a7413f --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj @@ -0,0 +1,56 @@ + + + + ILCompiler.ReadyToRun.Tests + $(NetCoreAppToolCurrent) + enable + false + true + x64;x86 + AnyCPU + linux-x64;win-x64;osx-x64 + Debug;Release;Checked + true + -notrait category=failing + + + + + + + + + + + + + + + + + + + + $(RuntimeBinDir)/crossgen2 + + + $(MicrosoftNetCoreAppRuntimePackRidLibTfmDir) + + + $(MicrosoftNetCoreAppRefPackRefDir) + + + $(CoreCLRArtifactsPath) + + + $(TargetArchitecture) + + + $(TargetOS) + + + $(Configuration) + + + + diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs new file mode 100644 index 00000000000000..b9efb76fab2646 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs @@ -0,0 +1,27 @@ +// Test: Async method thunks in R2R +// Validates that runtime-async compiled methods produce the expected +// RuntimeFunction layout (thunk + async body + resumption stub). +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncMethods +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task TestAsyncInline() + { + return await AsyncInlineableLib.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task TestAsyncStringInline() + { + return await AsyncInlineableLib.GetStringAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int TestSyncFromAsyncLib() + { + return AsyncInlineableLib.GetValueSync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs new file mode 100644 index 00000000000000..ba301272868c25 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs @@ -0,0 +1,26 @@ +// Test: Basic cross-module inlining +// Validates that crossgen2 with --opt-cross-module produces CHECK_IL_BODY fixups +// for methods inlined from InlineableLib into this main assembly. +using System; +using System.Runtime.CompilerServices; + +public static class BasicInlining +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static int TestGetValue() + { + return InlineableLib.GetValue(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static string TestGetString() + { + return InlineableLib.GetString(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int TestAdd() + { + return InlineableLib.Add(10, 32); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs new file mode 100644 index 00000000000000..0ba24d5563555c --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs @@ -0,0 +1,20 @@ +// Test: Composite mode basic compilation +// Validates that composite mode R2R compilation with multiple assemblies +// produces correct manifest references and component assembly entries. +using System; +using System.Runtime.CompilerServices; + +public static class CompositeBasic +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static int TestCompositeCall() + { + return CompositeLib.GetCompositeValue(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static object TestCompositeTypeCreation() + { + return new CompositeLib.CompositeType(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs new file mode 100644 index 00000000000000..153804e6279fc1 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncInlineableLib +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetValueAsync() => 42; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetStringAsync() => "Hello from async"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetValueSync() => 42; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs new file mode 100644 index 00000000000000..2dc5db2de38bcc --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs @@ -0,0 +1,11 @@ +using System; + +public static class CompositeLib +{ + public static int GetCompositeValue() => 100; + + public class CompositeType + { + public string Name { get; set; } = "Composite"; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs new file mode 100644 index 00000000000000..d56f2880564a13 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs @@ -0,0 +1,19 @@ +using System; + +public static class ExternalLib +{ + public static int ExternalValue => 99; + + public class ExternalType + { + public int Value { get; set; } + } + + public class Outer + { + public class Inner + { + public static int NestedValue => 77; + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs new file mode 100644 index 00000000000000..a799cfed7282e8 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.CompilerServices; + +public static class InlineableLib +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetValue() => 42; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetString() => "Hello from InlineableLib"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Add(int a, int b) => a + b; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs new file mode 100644 index 00000000000000..15fd29dda19d4b --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.CompilerServices; + +public static class InlineableLibTransitive +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetExternalValue() => ExternalLib.ExternalValue; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetNestedValue() => ExternalLib.Outer.Inner.NestedValue; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExternalLib.ExternalType CreateExternal() => new ExternalLib.ExternalType { Value = 42 }; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs new file mode 100644 index 00000000000000..25bac820fc3002 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs @@ -0,0 +1,26 @@ +// Test: Transitive cross-module references +// Validates that when InlineableLibTransitive is inlined, its references to ExternalLib +// are properly encoded in the R2R image (requiring tokens for both libraries). +using System; +using System.Runtime.CompilerServices; + +public static class TransitiveReferences +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static int TestTransitiveValue() + { + return InlineableLibTransitive.GetExternalValue(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int TestNestedTypeAccess() + { + return InlineableLibTransitive.GetNestedValue(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static object TestTransitiveTypeCreation() + { + return InlineableLibTransitive.CreateExternal(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs new file mode 100644 index 00000000000000..3134fcc2829466 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using ILCompiler.ReadyToRun.Tests.TestCasesRunner; +using Xunit; + +namespace ILCompiler.ReadyToRun.Tests.TestCases; + +/// +/// xUnit test suites for R2R cross-module resolution tests. +/// Each test method builds assemblies with Roslyn, crossgen2's them, and validates the R2R output. +/// +public class R2RTestSuites +{ + [Fact] + public void BasicCrossModuleInlining() + { + var expectations = new R2RExpectations(); + expectations.ExpectedManifestRefs.Add("InlineableLib"); + expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetValue")); + expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetString")); + expectations.Crossgen2Options.Add("--opt-cross-module:InlineableLib"); + + var testCase = new R2RTestCase + { + Name = "BasicCrossModuleInlining", + MainSourceResourceName = "CrossModuleInlining/BasicInlining.cs", + Dependencies = new List + { + new DependencyInfo + { + AssemblyName = "InlineableLib", + SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/InlineableLib.cs" }, + Crossgen = true, + } + }, + Expectations = expectations, + }; + + new R2RTestRunner().Run(testCase); + } + + [Fact] + public void TransitiveReferences() + { + var expectations = new R2RExpectations(); + expectations.ExpectedManifestRefs.Add("InlineableLibTransitive"); + expectations.ExpectedManifestRefs.Add("ExternalLib"); + expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetExternalValue")); + expectations.Crossgen2Options.Add("--opt-cross-module:InlineableLibTransitive"); + + var testCase = new R2RTestCase + { + Name = "TransitiveReferences", + MainSourceResourceName = "CrossModuleInlining/TransitiveReferences.cs", + Dependencies = new List + { + new DependencyInfo + { + AssemblyName = "ExternalLib", + SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/ExternalLib.cs" }, + Crossgen = false, + }, + new DependencyInfo + { + AssemblyName = "InlineableLibTransitive", + SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/InlineableLibTransitive.cs" }, + Crossgen = true, + AdditionalReferences = { "ExternalLib" }, + } + }, + Expectations = expectations, + }; + + new R2RTestRunner().Run(testCase); + } + + [Fact] + public void AsyncMethodThunks() + { + var expectations = new R2RExpectations + { + RuntimeAsync = true, + }; + expectations.ExpectedManifestRefs.Add("AsyncInlineableLib"); + expectations.ExpectedAsyncMethods.Add(new ExpectedAsyncMethod("TestAsyncInline", 3)); + expectations.Crossgen2Options.Add("--opt-cross-module:AsyncInlineableLib"); + + var testCase = new R2RTestCase + { + Name = "AsyncMethodThunks", + MainSourceResourceName = "CrossModuleInlining/AsyncMethods.cs", + Dependencies = new List + { + new DependencyInfo + { + AssemblyName = "AsyncInlineableLib", + SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/AsyncInlineableLib.cs" }, + Crossgen = true, + } + }, + Expectations = expectations, + }; + + new R2RTestRunner().Run(testCase); + } + + [Fact] + public void CompositeBasic() + { + var expectations = new R2RExpectations + { + CompositeMode = true, + }; + expectations.ExpectedManifestRefs.Add("CompositeLib"); + + var testCase = new R2RTestCase + { + Name = "CompositeBasic", + MainSourceResourceName = "CrossModuleInlining/CompositeBasic.cs", + Dependencies = new List + { + new DependencyInfo + { + AssemblyName = "CompositeLib", + SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/CompositeLib.cs" }, + Crossgen = true, + } + }, + Expectations = expectations, + }; + + new R2RTestRunner().Run(testCase); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs new file mode 100644 index 00000000000000..b1a731b62bcba2 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; + +/// +/// Result of a crossgen2 compilation step. +/// +internal sealed record R2RCompilationResult( + string OutputPath, + int ExitCode, + string StandardOutput, + string StandardError) +{ + public bool Success => ExitCode == 0; +} + +/// +/// Options for a single crossgen2 compilation step. +/// +internal sealed class R2RCompilationOptions +{ + public required string InputPath { get; init; } + public required string OutputPath { get; init; } + public List ReferencePaths { get; init; } = new(); + public List ExtraArgs { get; init; } = new(); + public bool Composite { get; init; } + public List? CompositeInputPaths { get; init; } + public List? InputBubbleRefs { get; init; } + public bool OptAsyncMethods { get; init; } +} + +/// +/// Invokes crossgen2 out-of-process to produce R2R images. +/// +internal sealed class R2RDriver +{ + private readonly string _crossgen2Dir; + + public R2RDriver() + { + _crossgen2Dir = TestPaths.Crossgen2Dir; + + if (!File.Exists(TestPaths.Crossgen2Dll)) + throw new FileNotFoundException($"crossgen2.dll not found at {TestPaths.Crossgen2Dll}"); + } + + /// + /// Runs crossgen2 on a single assembly. + /// + public R2RCompilationResult Compile(R2RCompilationOptions options) + { + var args = new List(); + + if (options.Composite) + { + args.Add("--composite"); + if (options.CompositeInputPaths is not null) + { + foreach (string input in options.CompositeInputPaths) + args.Add(input); + } + } + else + { + args.Add(options.InputPath); + } + + args.Add("-o"); + args.Add(options.OutputPath); + + foreach (string refPath in options.ReferencePaths) + { + args.Add("-r"); + args.Add(refPath); + } + + if (options.InputBubbleRefs is not null) + { + foreach (string bubbleRef in options.InputBubbleRefs) + { + args.Add("--inputbubbleref"); + args.Add(bubbleRef); + } + } + + if (options.OptAsyncMethods) + { + args.Add("--opt-async-methods"); + } + + args.AddRange(options.ExtraArgs); + + return RunCrossgen2(args); + } + + /// + /// Crossgen2 a dependency assembly (simple single-assembly R2R). + /// + public R2RCompilationResult CompileDependency(string inputPath, string outputPath, IEnumerable referencePaths) + { + return Compile(new R2RCompilationOptions + { + InputPath = inputPath, + OutputPath = outputPath, + ReferencePaths = referencePaths.ToList() + }); + } + + private R2RCompilationResult RunCrossgen2(List crossgen2Args) + { + // Use dotnet exec to invoke crossgen2.dll + string dotnetHost = TestPaths.DotNetHost; + string crossgen2Dll = TestPaths.Crossgen2Dll; + + var allArgs = new List { "exec", crossgen2Dll }; + allArgs.AddRange(crossgen2Args); + + string argsString = string.Join(" ", allArgs.Select(QuoteIfNeeded)); + + var psi = new ProcessStartInfo + { + FileName = dotnetHost, + Arguments = argsString, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + // Strip environment variables that interfere with crossgen2 + string[] envVarsToStrip = { "DOTNET_GCName", "DOTNET_GCStress", "DOTNET_HeapVerify", "DOTNET_ReadyToRun" }; + foreach (string envVar in envVarsToStrip) + { + psi.Environment[envVar] = null; + } + + using var process = Process.Start(psi)!; + string stdout = process.StandardOutput.ReadToEnd(); + string stderr = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + string outputPath = crossgen2Args + .SkipWhile(a => a != "-o") + .Skip(1) + .FirstOrDefault() ?? "unknown"; + + return new R2RCompilationResult( + outputPath, + process.ExitCode, + stdout, + stderr); + } + + private static string QuoteIfNeeded(string arg) + { + if (arg.Contains(' ') || arg.Contains('"')) + return $"\"{arg.Replace("\"", "\\\"")}\""; + return arg; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs new file mode 100644 index 00000000000000..5ef3297b00fed8 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -0,0 +1,233 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using ILCompiler.Reflection.ReadyToRun; +using Xunit; + +namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; + +/// +/// Parsed expectations from a test case's assembly-level and method-level attributes. +/// +internal sealed class R2RExpectations +{ + public List ExpectedManifestRefs { get; } = new(); + public List ExpectedInlinedMethods { get; } = new(); + public List ExpectedAsyncMethods { get; } = new(); + public bool CompositeMode { get; set; } + public bool RuntimeAsync { get; set; } + public List Crossgen2Options { get; } = new(); +} + +internal sealed record ExpectedInlinedMethod(string MethodName); +internal sealed record ExpectedAsyncMethod(string MethodName, int ExpectedRuntimeFunctionCount); + +/// +/// Validates R2R images against test expectations using ReadyToRunReader. +/// +internal sealed class R2RResultChecker +{ + /// + /// Validates the main R2R image against expectations. + /// + public void Check(string r2rImagePath, R2RExpectations expectations) + { + Assert.True(File.Exists(r2rImagePath), $"R2R image not found: {r2rImagePath}"); + + using var fileStream = File.OpenRead(r2rImagePath); + using var peReader = new PEReader(fileStream); + + Assert.True(ReadyToRunReader.IsReadyToRunImage(peReader), + $"'{Path.GetFileName(r2rImagePath)}' is not a valid R2R image"); + + var reader = new ReadyToRunReader(new SimpleAssemblyResolver(), r2rImagePath); + + CheckManifestRefs(reader, expectations, r2rImagePath); + CheckInlinedMethods(reader, expectations, r2rImagePath); + CheckAsyncMethods(reader, expectations, r2rImagePath); + } + + private static void CheckManifestRefs(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + { + if (expectations.ExpectedManifestRefs.Count == 0) + return; + + // Get all assembly references (both MSIL and manifest) + var allRefs = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Read MSIL AssemblyRef table + var globalMetadata = reader.GetGlobalMetadata(); + var mdReader = globalMetadata.MetadataReader; + foreach (var handle in mdReader.AssemblyReferences) + { + var assemblyRef = mdReader.GetAssemblyReference(handle); + string name = mdReader.GetString(assemblyRef.Name); + allRefs.Add(name); + } + + // Read manifest references (extra refs beyond MSIL table) + foreach (var kvp in reader.ManifestReferenceAssemblies) + { + allRefs.Add(kvp.Key); + } + + foreach (string expected in expectations.ExpectedManifestRefs) + { + Assert.True(allRefs.Contains(expected), + $"Expected assembly reference '{expected}' not found in R2R image '{Path.GetFileName(imagePath)}'. " + + $"Found: [{string.Join(", ", allRefs.OrderBy(s => s))}]"); + } + } + + private static void CheckInlinedMethods(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + { + if (expectations.ExpectedInlinedMethods.Count == 0) + return; + + // Collect all fixup info: look for CHECK_IL_BODY fixup kind + var checkIlBodySignatures = new HashSet(StringComparer.OrdinalIgnoreCase); + var allMethodNames = new List(); + var allFixups = new List(); + var formattingOptions = new SignatureFormattingOptions(); + + void CollectFixups(ReadyToRunMethod method) + { + allMethodNames.Add(method.SignatureString); + foreach (var cell in method.Fixups) + { + if (cell.Signature is null) + continue; + + string sigText = cell.Signature.ToString(formattingOptions); + allFixups.Add($"{method.SignatureString} -> [{cell.Signature.FixupKind}] {sigText}"); + + if (cell.Signature.FixupKind is ReadyToRunFixupKind.Check_IL_Body or ReadyToRunFixupKind.Verify_IL_Body) + { + checkIlBodySignatures.Add(sigText); + } + } + } + + foreach (var assembly in reader.ReadyToRunAssemblies) + { + foreach (var method in assembly.Methods) + CollectFixups(method); + } + + foreach (var instanceMethod in reader.InstanceMethods) + CollectFixups(instanceMethod.Method); + + foreach (var expected in expectations.ExpectedInlinedMethods) + { + bool found = checkIlBodySignatures.Any(f => + f.Contains(expected.MethodName, StringComparison.OrdinalIgnoreCase)); + + Assert.True(found, + $"Expected CHECK_IL_BODY fixup for '{expected.MethodName}' not found in '{Path.GetFileName(imagePath)}'. " + + $"CHECK_IL_BODY fixups: [{string.Join(", ", checkIlBodySignatures)}]. " + + $"All methods ({allMethodNames.Count}): [{string.Join(", ", allMethodNames.Take(30))}]. " + + $"All fixups ({allFixups.Count}): [{string.Join("; ", allFixups.Take(30))}]"); + } + } + + private static void CheckAsyncMethods(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + { + if (expectations.ExpectedAsyncMethods.Count == 0) + return; + + // Build method name -> RuntimeFunction count map from all sources + var methodFunctionCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var assembly in reader.ReadyToRunAssemblies) + { + foreach (var method in assembly.Methods) + { + methodFunctionCounts[method.SignatureString] = method.RuntimeFunctions.Count; + } + } + + foreach (var instanceMethod in reader.InstanceMethods) + { + methodFunctionCounts[instanceMethod.Method.SignatureString] = instanceMethod.Method.RuntimeFunctions.Count; + } + + foreach (var expected in expectations.ExpectedAsyncMethods) + { + var match = methodFunctionCounts + .FirstOrDefault(kvp => kvp.Key.Contains(expected.MethodName, StringComparison.OrdinalIgnoreCase)); + + Assert.True(match.Key is not null, + $"Expected async method '{expected.MethodName}' not found in R2R image '{Path.GetFileName(imagePath)}'. " + + $"Found methods: [{string.Join(", ", methodFunctionCounts.Keys.Take(20))}...]"); + + Assert.True(match.Value >= expected.ExpectedRuntimeFunctionCount, + $"Async method '{expected.MethodName}' has {match.Value} RuntimeFunctions, " + + $"expected >= {expected.ExpectedRuntimeFunctionCount} in '{Path.GetFileName(imagePath)}'"); + } + } +} + +/// +/// Simple assembly resolver that looks in the same directory as the input image. +/// +internal sealed class SimpleAssemblyResolver : IAssemblyResolver +{ + private readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase); + + public IAssemblyMetadata? FindAssembly(MetadataReader metadataReader, AssemblyReferenceHandle assemblyReferenceHandle, string parentFile) + { + var assemblyRef = metadataReader.GetAssemblyReference(assemblyReferenceHandle); + string name = metadataReader.GetString(assemblyRef.Name); + return FindAssembly(name, parentFile); + } + + public IAssemblyMetadata? FindAssembly(string simpleName, string parentFile) + { + string? dir = Path.GetDirectoryName(parentFile); + if (dir is null) + return null; + + string candidate = Path.Combine(dir, simpleName + ".dll"); + if (!File.Exists(candidate)) + { + // Try in runtime pack + candidate = Path.Combine(TestPaths.RuntimePackDir, simpleName + ".dll"); + } + + if (!File.Exists(candidate)) + return null; + + return new SimpleAssemblyMetadata(candidate); + } +} + +/// +/// Simple assembly metadata wrapper. +/// +internal sealed class SimpleAssemblyMetadata : IAssemblyMetadata, IDisposable +{ + private readonly FileStream _stream; + private readonly PEReader _peReader; + + public SimpleAssemblyMetadata(string path) + { + _stream = File.OpenRead(path); + _peReader = new PEReader(_stream); + } + + public PEReader ImageReader => _peReader; + + public MetadataReader MetadataReader => _peReader.GetMetadataReader(); + + public void Dispose() + { + _peReader.Dispose(); + _stream.Dispose(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs new file mode 100644 index 00000000000000..8cbbc439d6cfb8 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; + +namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; + +/// +/// Compiles C# source code into assemblies using Roslyn at test time. +/// +internal sealed class R2RTestCaseCompiler +{ + private readonly string _outputDir; + private readonly List _frameworkReferences; + + public R2RTestCaseCompiler(string outputDir) + { + _outputDir = outputDir; + _frameworkReferences = new List(); + + // Add reference assemblies from the ref pack (needed for Roslyn compilation) + string refPackDir = TestPaths.RefPackDir; + if (Directory.Exists(refPackDir)) + { + foreach (string refPath in Directory.EnumerateFiles(refPackDir, "*.dll")) + { + _frameworkReferences.Add(MetadataReference.CreateFromFile(refPath)); + } + } + else + { + // Fallback to runtime pack implementation assemblies + foreach (string refPath in TestPaths.GetFrameworkReferencePaths()) + { + _frameworkReferences.Add(MetadataReference.CreateFromFile(refPath)); + } + } + } + + /// + /// Compiles a single assembly from source files. + /// + /// Name of the output assembly (without .dll extension). + /// C# source code strings. + /// Paths to additional assembly references. + /// Library or ConsoleApplication. + /// Additional preprocessor defines. + /// Path to the compiled assembly. + public string CompileAssembly( + string assemblyName, + IEnumerable sources, + IEnumerable? additionalReferences = null, + OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, + IEnumerable? additionalDefines = null) + { + var syntaxTrees = sources.Select(src => + CSharpSyntaxTree.ParseText(src, new CSharpParseOptions( + LanguageVersion.Latest, + preprocessorSymbols: additionalDefines))); + + var references = new List(_frameworkReferences); + if (additionalReferences is not null) + { + foreach (string refPath in additionalReferences) + { + references.Add(MetadataReference.CreateFromFile(refPath)); + } + } + + var compilation = CSharpCompilation.Create( + assemblyName, + syntaxTrees, + references, + new CSharpCompilationOptions(outputKind) + .WithOptimizationLevel(OptimizationLevel.Release) + .WithAllowUnsafe(true) + .WithNullableContextOptions(NullableContextOptions.Enable)); + + string outputPath = Path.Combine(_outputDir, assemblyName + ".dll"); + EmitResult result = compilation.Emit(outputPath); + + if (!result.Success) + { + var errors = result.Diagnostics + .Where(d => d.Severity == DiagnosticSeverity.Error) + .Select(d => d.ToString()); + throw new InvalidOperationException( + $"Compilation of '{assemblyName}' failed:\n{string.Join("\n", errors)}"); + } + + return outputPath; + } + + /// + /// Reads an embedded resource from the test assembly. + /// + public static string ReadEmbeddedSource(string resourceName) + { + var assembly = Assembly.GetExecutingAssembly(); + using Stream? stream = assembly.GetManifestResourceStream(resourceName); + if (stream is null) + { + // Try with different path separator + string altName = resourceName.Replace('/', '\\'); + using Stream? altStream = assembly.GetManifestResourceStream(altName); + if (altStream is null) + throw new FileNotFoundException($"Embedded resource not found: '{resourceName}'. Available: {string.Join(", ", assembly.GetManifestResourceNames())}"); + + using var altReader = new StreamReader(altStream); + return altReader.ReadToEnd(); + } + + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs new file mode 100644 index 00000000000000..7e1057bd9f6e42 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs @@ -0,0 +1,204 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ILCompiler.ReadyToRun.Tests.Expectations; +using Xunit; + +namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; + +/// +/// Describes a test case: a main source file with its dependencies and expectations. +/// +internal sealed class R2RTestCase +{ + public required string Name { get; init; } + public required string MainSourceResourceName { get; init; } + public required List Dependencies { get; init; } + public required R2RExpectations Expectations { get; init; } +} + +/// +/// Describes a dependency assembly for a test case. +/// +internal sealed class DependencyInfo +{ + public required string AssemblyName { get; init; } + public required string[] SourceResourceNames { get; init; } + public bool Crossgen { get; init; } + public List CrossgenOptions { get; init; } = new(); + public List AdditionalReferences { get; init; } = new(); +} + +/// +/// Orchestrates the full R2R test pipeline: compile → crossgen2 → validate. +/// +internal sealed class R2RTestRunner +{ + /// + /// Runs a test case end-to-end. + /// + public void Run(R2RTestCase testCase) + { + string tempDir = Path.Combine(Path.GetTempPath(), "R2RTests", testCase.Name, Guid.NewGuid().ToString("N")[..8]); + string ilDir = Path.Combine(tempDir, "il"); + string r2rDir = Path.Combine(tempDir, "r2r"); + + try + { + Directory.CreateDirectory(ilDir); + Directory.CreateDirectory(r2rDir); + + // Step 1: Compile all dependencies with Roslyn + var compiler = new R2RTestCaseCompiler(ilDir); + var compiledDeps = new List<(DependencyInfo Dep, string IlPath)>(); + + foreach (var dep in testCase.Dependencies) + { + var sources = dep.SourceResourceNames + .Select(R2RTestCaseCompiler.ReadEmbeddedSource) + .ToList(); + + var refs = dep.AdditionalReferences + .Select(r => compiledDeps.First(d => d.Dep.AssemblyName == r).IlPath) + .ToList(); + + string ilPath = compiler.CompileAssembly(dep.AssemblyName, sources, refs); + compiledDeps.Add((dep, ilPath)); + } + + // Step 2: Compile main assembly with Roslyn + string mainSource = R2RTestCaseCompiler.ReadEmbeddedSource(testCase.MainSourceResourceName); + var mainRefs = compiledDeps.Select(d => d.IlPath).ToList(); + string mainIlPath = compiler.CompileAssembly(testCase.Name, new[] { mainSource }, mainRefs, + outputKind: Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary); + + // Step 3: Crossgen2 dependencies + var driver = new R2RDriver(); + var allRefPaths = BuildReferencePaths(ilDir); + + foreach (var (dep, ilPath) in compiledDeps) + { + if (!dep.Crossgen) + continue; + + string r2rPath = Path.Combine(r2rDir, Path.GetFileName(ilPath)); + var result = driver.Compile(new R2RCompilationOptions + { + InputPath = ilPath, + OutputPath = r2rPath, + ReferencePaths = allRefPaths, + ExtraArgs = dep.CrossgenOptions, + }); + + Assert.True(result.Success, + $"crossgen2 failed for dependency '{dep.AssemblyName}':\n{result.StandardError}\n{result.StandardOutput}"); + } + + // Step 4: Crossgen2 main assembly + string mainR2RPath = Path.Combine(r2rDir, Path.GetFileName(mainIlPath)); + + if (testCase.Expectations.CompositeMode) + { + RunCompositeCompilation(testCase, driver, ilDir, r2rDir, mainIlPath, mainR2RPath, allRefPaths, compiledDeps); + } + else + { + RunSingleCompilation(testCase, driver, mainIlPath, mainR2RPath, allRefPaths); + } + + // Step 5: Validate R2R output + var checker = new R2RResultChecker(); + checker.Check(mainR2RPath, testCase.Expectations); + } + finally + { + // Keep temp directory for debugging if KEEP_R2R_TESTS env var is set + if (Environment.GetEnvironmentVariable("KEEP_R2R_TESTS") is null) + { + try { Directory.Delete(tempDir, true); } + catch { /* best effort */ } + } + } + } + + private static void RunSingleCompilation( + R2RTestCase testCase, + R2RDriver driver, + string mainIlPath, + string mainR2RPath, + List allRefPaths) + { + var options = new R2RCompilationOptions + { + InputPath = mainIlPath, + OutputPath = mainR2RPath, + ReferencePaths = allRefPaths, + ExtraArgs = testCase.Expectations.Crossgen2Options.ToList(), + OptAsyncMethods = testCase.Expectations.RuntimeAsync, + }; + + var result = driver.Compile(options); + Assert.True(result.Success, + $"crossgen2 failed for main assembly '{testCase.Name}':\n{result.StandardError}\n{result.StandardOutput}"); + } + + private static void RunCompositeCompilation( + R2RTestCase testCase, + R2RDriver driver, + string ilDir, + string r2rDir, + string mainIlPath, + string mainR2RPath, + List allRefPaths, + List<(DependencyInfo Dep, string IlPath)> compiledDeps) + { + var compositeInputs = new List { mainIlPath }; + foreach (var (dep, ilPath) in compiledDeps) + { + if (dep.Crossgen) + compositeInputs.Add(ilPath); + } + + var options = new R2RCompilationOptions + { + InputPath = mainIlPath, + OutputPath = mainR2RPath, + ReferencePaths = allRefPaths, + Composite = true, + CompositeInputPaths = compositeInputs, + ExtraArgs = testCase.Expectations.Crossgen2Options.ToList(), + OptAsyncMethods = testCase.Expectations.RuntimeAsync, + }; + + var result = driver.Compile(options); + Assert.True(result.Success, + $"crossgen2 composite compilation failed for '{testCase.Name}':\n{result.StandardError}\n{result.StandardOutput}"); + } + + private static List BuildReferencePaths(string ilDir) + { + var paths = new List(); + + // Add all compiled IL assemblies as references + paths.Add(Path.Combine(ilDir, "*.dll")); + + // Add framework references (managed assemblies) + paths.Add(Path.Combine(TestPaths.RuntimePackDir, "*.dll")); + + // System.Private.CoreLib is in the native directory, not lib + string runtimePackDir = TestPaths.RuntimePackDir; + string nativeDir = Path.GetFullPath(Path.Combine(runtimePackDir, "..", "..", "native")); + if (Directory.Exists(nativeDir)) + { + string spcl = Path.Combine(nativeDir, "System.Private.CoreLib.dll"); + if (File.Exists(spcl)) + paths.Add(spcl); + } + + return paths; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs new file mode 100644 index 00000000000000..86cfeefda559e2 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs @@ -0,0 +1,183 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; + +/// +/// Provides paths to build artifacts needed by the test infrastructure. +/// All paths come from RuntimeHostConfigurationOption items in the csproj. +/// +internal static class TestPaths +{ + private static string GetRequiredConfig(string key) + { + return AppContext.GetData(key) as string + ?? throw new InvalidOperationException($"Missing RuntimeHostConfigurationOption '{key}'. Was the project built with the correct properties?"); + } + + /// + /// Path to the crossgen2 output directory (contains crossgen2.dll and clrjit). + /// e.g. artifacts/bin/coreclr/linux.x64.Checked/crossgen2/ + /// Falls back to Checked or Release if Debug path doesn't exist. + /// + public static string Crossgen2Dir + { + get + { + string dir = GetRequiredConfig("R2RTest.Crossgen2Dir"); + if (!Directory.Exists(dir)) + { + // Try Checked and Release fallbacks since crossgen2 may be built in a different config + foreach (string fallbackConfig in new[] { "Checked", "Release", "Debug" }) + { + string fallback = Regex.Replace( + dir, @"\.(Debug|Release|Checked)[/\\]", $".{fallbackConfig}/"); + if (Directory.Exists(fallback)) + return fallback; + } + } + + return dir; + } + } + + /// + /// Path to the crossgen2.dll managed assembly. + /// + public static string Crossgen2Dll => Path.Combine(Crossgen2Dir, "crossgen2.dll"); + + /// + /// Path to the runtime pack managed assemblies directory. + /// e.g. artifacts/bin/microsoft.netcore.app.runtime.linux-x64/Release/runtimes/linux-x64/lib/net11.0/ + /// Falls back to Release if Debug path doesn't exist (libs are typically built Release). + /// + public static string RuntimePackDir + { + get + { + string dir = GetRequiredConfig("R2RTest.RuntimePackDir"); + if (!Directory.Exists(dir) && dir.Contains("Debug")) + { + string releaseFallback = dir.Replace("Debug", "Release"); + if (Directory.Exists(releaseFallback)) + return releaseFallback; + } + + return dir; + } + } + + /// + /// Path to the CoreCLR artifacts directory (contains native bits like corerun). + /// e.g. artifacts/bin/coreclr/linux.x64.Checked/ + /// Falls back to Checked or Release if Debug path doesn't exist. + /// + public static string CoreCLRArtifactsDir + { + get + { + string dir = GetRequiredConfig("R2RTest.CoreCLRArtifactsDir"); + if (!Directory.Exists(dir)) + { + foreach (string fallbackConfig in new[] { "Checked", "Release", "Debug" }) + { + string fallback = Regex.Replace( + dir, @"\.(Debug|Release|Checked)(/|\\|$)", $".{fallbackConfig}$2"); + if (Directory.Exists(fallback)) + return fallback; + } + } + + return dir; + } + } + + public static string TargetArchitecture => GetRequiredConfig("R2RTest.TargetArchitecture"); + public static string TargetOS => GetRequiredConfig("R2RTest.TargetOS"); + public static string Configuration => GetRequiredConfig("R2RTest.Configuration"); + + /// + /// Path to the reference assembly pack (for Roslyn compilation). + /// e.g. artifacts/bin/microsoft.netcore.app.ref/ref/net11.0/ + /// + public static string RefPackDir + { + get + { + string dir = GetRequiredConfig("R2RTest.RefPackDir"); + if (!Directory.Exists(dir)) + { + // Try the artifacts/bin/ref/net* fallback + string artifactsBin = Path.GetFullPath(Path.Combine(CoreCLRArtifactsDir, "..", "..")); + string refDir = Path.Combine(artifactsBin, "ref"); + if (Directory.Exists(refDir)) + { + foreach (string subDir in Directory.GetDirectories(refDir, "net*")) + { + if (File.Exists(Path.Combine(subDir, "System.Runtime.dll"))) + return subDir; + } + } + } + + return dir; + } + } + + /// + /// Returns the dotnet host executable path suitable for running crossgen2. + /// + public static string DotNetHost + { + get + { + string repoRoot = Path.GetFullPath(Path.Combine(CoreCLRArtifactsDir, "..", "..", "..", "..")); + string dotnetDir = Path.Combine(repoRoot, ".dotnet"); + string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; + string path = Path.Combine(dotnetDir, exe); + if (File.Exists(path)) + return path; + + // Fallback to PATH + return exe; + } + } + + /// + /// Returns the corerun executable path. + /// + public static string CoreRun + { + get + { + string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "corerun.exe" : "corerun"; + string path = Path.Combine(CoreCLRArtifactsDir, exe); + if (File.Exists(path)) + return path; + + throw new FileNotFoundException($"corerun not found at {path}"); + } + } + + /// + /// Returns the target triple string for crossgen2 (e.g. "linux-x64"). + /// + public static string TargetTriple => $"{TargetOS.ToLowerInvariant()}-{TargetArchitecture.ToLowerInvariant()}"; + + /// + /// Returns all framework reference assembly paths (*.dll in the runtime pack). + /// + public static IEnumerable GetFrameworkReferencePaths() + { + if (!Directory.Exists(RuntimePackDir)) + throw new DirectoryNotFoundException($"Runtime pack directory not found: {RuntimePackDir}"); + + return Directory.EnumerateFiles(RuntimePackDir, "*.dll"); + } +} diff --git a/src/coreclr/tools/aot/crossgen2.slnx b/src/coreclr/tools/aot/crossgen2.slnx index b79a67596c466f..b2cd7b76b2bf64 100644 --- a/src/coreclr/tools/aot/crossgen2.slnx +++ b/src/coreclr/tools/aot/crossgen2.slnx @@ -46,6 +46,9 @@ + + + From 3e1a6c8bd4dbdf3e8232ef3f1b0015b6148a522f Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:52:49 -0700 Subject: [PATCH 12/74] Fix R2R test validation and remove opt-async-methods - Use FixupKind enum + ToString(SignatureFormattingOptions) instead of ToString() which returned the class name for TodoSignature fixups - Add System.Private.CoreLib from native/ dir to crossgen2 references - Use dotnet exec instead of corerun to invoke crossgen2 - Remove opt-async-methods plumbing (unrelated to runtime-async) - Rename AsyncMethodThunks to AsyncCrossModuleInlining with correct expectations (CHECK_IL_BODY fixups, not RuntimeFunction counts) - Remove unused CoreRun property from TestPaths - Add KEEP_R2R_TESTS env var to preserve temp dirs for debugging All 4 tests pass: BasicCrossModuleInlining, TransitiveReferences, AsyncCrossModuleInlining, CompositeBasic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Expectations/R2RExpectationAttributes.cs | 21 ++------ .../TestCases/R2RTestSuites.cs | 11 ++-- .../TestCasesRunner/R2RDriver.cs | 6 --- .../TestCasesRunner/R2RResultChecker.cs | 51 ++----------------- .../TestCasesRunner/R2RTestRunner.cs | 2 - .../TestCasesRunner/TestPaths.cs | 16 ------ 6 files changed, 11 insertions(+), 96 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs index 13a8edf0ad7122..8fe039762c1ce7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs @@ -22,18 +22,13 @@ public sealed class ExpectInlinedAttribute : Attribute } /// -/// Marks a method as expected to have async thunk RuntimeFunctions in the R2R image. -/// Async methods compiled with --opt-async-methods produce 3 RuntimeFunctions: -/// thunk + async body + resumption stub. +/// Marks a method as expected to have a specific number of RuntimeFunctions in the R2R image. /// [Conditional("R2R_EXPECTATIONS")] [AttributeUsage(AttributeTargets.Method)] -public sealed class ExpectAsyncThunkAttribute : Attribute +public sealed class ExpectRuntimeFunctionCountAttribute : Attribute { - /// - /// Expected number of RuntimeFunctions. Defaults to 3 (thunk + body + resumption). - /// - public int ExpectedRuntimeFunctionCount { get; set; } = 3; + public int ExpectedCount { get; set; } } /// @@ -126,13 +121,3 @@ public sealed class ExpectR2RMethodAttribute : Attribute public sealed class CompositeModeAttribute : Attribute { } - -/// -/// Marks an assembly-level option to enable runtime-async compilation. -/// Adds Features=runtime-async=on to Roslyn compilation and --opt-async-methods to crossgen2. -/// -[Conditional("R2R_EXPECTATIONS")] -[AttributeUsage(AttributeTargets.Assembly)] -public sealed class EnableRuntimeAsyncAttribute : Attribute -{ -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 3134fcc2829466..2ca47891e59f66 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -77,19 +77,16 @@ public void TransitiveReferences() } [Fact] - public void AsyncMethodThunks() + public void AsyncCrossModuleInlining() { - var expectations = new R2RExpectations - { - RuntimeAsync = true, - }; + var expectations = new R2RExpectations(); expectations.ExpectedManifestRefs.Add("AsyncInlineableLib"); - expectations.ExpectedAsyncMethods.Add(new ExpectedAsyncMethod("TestAsyncInline", 3)); + expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetValueAsync")); expectations.Crossgen2Options.Add("--opt-cross-module:AsyncInlineableLib"); var testCase = new R2RTestCase { - Name = "AsyncMethodThunks", + Name = "AsyncCrossModuleInlining", MainSourceResourceName = "CrossModuleInlining/AsyncMethods.cs", Dependencies = new List { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs index b1a731b62bcba2..6aa4f9d3479cbb 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs @@ -34,7 +34,6 @@ internal sealed class R2RCompilationOptions public bool Composite { get; init; } public List? CompositeInputPaths { get; init; } public List? InputBubbleRefs { get; init; } - public bool OptAsyncMethods { get; init; } } /// @@ -91,11 +90,6 @@ public R2RCompilationResult Compile(R2RCompilationOptions options) } } - if (options.OptAsyncMethods) - { - args.Add("--opt-async-methods"); - } - args.AddRange(options.ExtraArgs); return RunCrossgen2(args); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index 5ef3297b00fed8..d7694a3e1998e1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -8,6 +8,7 @@ using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using ILCompiler.Reflection.ReadyToRun; +using Internal.ReadyToRunConstants; using Xunit; namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; @@ -19,14 +20,11 @@ internal sealed class R2RExpectations { public List ExpectedManifestRefs { get; } = new(); public List ExpectedInlinedMethods { get; } = new(); - public List ExpectedAsyncMethods { get; } = new(); public bool CompositeMode { get; set; } - public bool RuntimeAsync { get; set; } public List Crossgen2Options { get; } = new(); } internal sealed record ExpectedInlinedMethod(string MethodName); -internal sealed record ExpectedAsyncMethod(string MethodName, int ExpectedRuntimeFunctionCount); /// /// Validates R2R images against test expectations using ReadyToRunReader. @@ -50,7 +48,6 @@ public void Check(string r2rImagePath, R2RExpectations expectations) CheckManifestRefs(reader, expectations, r2rImagePath); CheckInlinedMethods(reader, expectations, r2rImagePath); - CheckAsyncMethods(reader, expectations, r2rImagePath); } private static void CheckManifestRefs(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) @@ -90,22 +87,19 @@ private static void CheckInlinedMethods(ReadyToRunReader reader, R2RExpectations if (expectations.ExpectedInlinedMethods.Count == 0) return; - // Collect all fixup info: look for CHECK_IL_BODY fixup kind var checkIlBodySignatures = new HashSet(StringComparer.OrdinalIgnoreCase); - var allMethodNames = new List(); - var allFixups = new List(); var formattingOptions = new SignatureFormattingOptions(); + var allFixupSummary = new List(); void CollectFixups(ReadyToRunMethod method) { - allMethodNames.Add(method.SignatureString); foreach (var cell in method.Fixups) { if (cell.Signature is null) continue; string sigText = cell.Signature.ToString(formattingOptions); - allFixups.Add($"{method.SignatureString} -> [{cell.Signature.FixupKind}] {sigText}"); + allFixupSummary.Add($"[{cell.Signature.FixupKind}] {sigText}"); if (cell.Signature.FixupKind is ReadyToRunFixupKind.Check_IL_Body or ReadyToRunFixupKind.Verify_IL_Body) { @@ -131,44 +125,7 @@ void CollectFixups(ReadyToRunMethod method) Assert.True(found, $"Expected CHECK_IL_BODY fixup for '{expected.MethodName}' not found in '{Path.GetFileName(imagePath)}'. " + $"CHECK_IL_BODY fixups: [{string.Join(", ", checkIlBodySignatures)}]. " + - $"All methods ({allMethodNames.Count}): [{string.Join(", ", allMethodNames.Take(30))}]. " + - $"All fixups ({allFixups.Count}): [{string.Join("; ", allFixups.Take(30))}]"); - } - } - - private static void CheckAsyncMethods(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) - { - if (expectations.ExpectedAsyncMethods.Count == 0) - return; - - // Build method name -> RuntimeFunction count map from all sources - var methodFunctionCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var assembly in reader.ReadyToRunAssemblies) - { - foreach (var method in assembly.Methods) - { - methodFunctionCounts[method.SignatureString] = method.RuntimeFunctions.Count; - } - } - - foreach (var instanceMethod in reader.InstanceMethods) - { - methodFunctionCounts[instanceMethod.Method.SignatureString] = instanceMethod.Method.RuntimeFunctions.Count; - } - - foreach (var expected in expectations.ExpectedAsyncMethods) - { - var match = methodFunctionCounts - .FirstOrDefault(kvp => kvp.Key.Contains(expected.MethodName, StringComparison.OrdinalIgnoreCase)); - - Assert.True(match.Key is not null, - $"Expected async method '{expected.MethodName}' not found in R2R image '{Path.GetFileName(imagePath)}'. " + - $"Found methods: [{string.Join(", ", methodFunctionCounts.Keys.Take(20))}...]"); - - Assert.True(match.Value >= expected.ExpectedRuntimeFunctionCount, - $"Async method '{expected.MethodName}' has {match.Value} RuntimeFunctions, " + - $"expected >= {expected.ExpectedRuntimeFunctionCount} in '{Path.GetFileName(imagePath)}'"); + $"All fixups: [{string.Join("; ", allFixupSummary)}]"); } } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs index 7e1057bd9f6e42..1c372ce8c3176c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs @@ -138,7 +138,6 @@ private static void RunSingleCompilation( OutputPath = mainR2RPath, ReferencePaths = allRefPaths, ExtraArgs = testCase.Expectations.Crossgen2Options.ToList(), - OptAsyncMethods = testCase.Expectations.RuntimeAsync, }; var result = driver.Compile(options); @@ -171,7 +170,6 @@ private static void RunCompositeCompilation( Composite = true, CompositeInputPaths = compositeInputs, ExtraArgs = testCase.Expectations.Crossgen2Options.ToList(), - OptAsyncMethods = testCase.Expectations.RuntimeAsync, }; var result = driver.Compile(options); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs index 86cfeefda559e2..2524ae4fd867fd 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs @@ -149,22 +149,6 @@ public static string DotNetHost } } - /// - /// Returns the corerun executable path. - /// - public static string CoreRun - { - get - { - string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "corerun.exe" : "corerun"; - string path = Path.Combine(CoreCLRArtifactsDir, exe); - if (File.Exists(path)) - return path; - - throw new FileNotFoundException($"corerun not found at {path}"); - } - } - /// /// Returns the target triple string for crossgen2 (e.g. "linux-x64"). /// From 183262c73bde9ca339b8fb5a81f2e2c494d1be59 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:13:49 -0700 Subject: [PATCH 13/74] Add runtime-async functional tests for R2R Add 5 new tests covering key runtime-async crossgen2 PRs: - RuntimeAsyncMethodEmission (#124203): Validates [ASYNC] variant entries for Task/ValueTask-returning methods - RuntimeAsyncContinuationLayout (#123643): Validates ContinuationLayout and ResumptionStubEntryPoint fixups for methods with GC refs across awaits - RuntimeAsyncDevirtualize (#125420): Validates async virtual method devirtualization produces [ASYNC] entries for sealed/interface dispatch - RuntimeAsyncNoYield (#124203): Validates async methods without await still produce [ASYNC] variants - RuntimeAsyncCrossModule (#121679): Validates MutableModule async references work with cross-module inlining of runtime-async methods Infrastructure changes: - Add Roslyn feature flag support (runtime-async=on) to R2RTestCaseCompiler - Add R2RExpectations for async variants, resumption stubs, continuation layouts, and arbitrary fixup kinds - Add MainExtraSourceResourceNames to R2RTestCase for shared source files - Add null guard for method.Fixups in CheckFixupKinds All 9 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 153 ++++++++++++++++++ .../RuntimeAsync/AsyncCrossModule.cs | 28 ++++ .../RuntimeAsync/AsyncDevirtualize.cs | 56 +++++++ .../TestCases/RuntimeAsync/AsyncNoYield.cs | 23 +++ .../RuntimeAsync/AsyncWithContinuation.cs | 26 +++ .../RuntimeAsync/BasicAsyncEmission.cs | 36 +++++ .../RuntimeAsync/Dependencies/AsyncDepLib.cs | 28 ++++ .../RuntimeAsyncMethodGenerationAttribute.cs | 10 ++ .../TestCasesRunner/R2RResultChecker.cs | 128 +++++++++++++++ .../TestCasesRunner/R2RTestCaseCompiler.cs | 15 +- .../TestCasesRunner/R2RTestRunner.cs | 27 +++- 11 files changed, 522 insertions(+), 8 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 2ca47891e59f66..620c7da3dc1766 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; +using Internal.ReadyToRunConstants; using Xunit; namespace ILCompiler.ReadyToRun.Tests.TestCases; @@ -13,6 +14,8 @@ namespace ILCompiler.ReadyToRun.Tests.TestCases; /// public class R2RTestSuites { + private static readonly KeyValuePair RuntimeAsyncFeature = new("runtime-async", "on"); + [Fact] public void BasicCrossModuleInlining() { @@ -130,4 +133,154 @@ public void CompositeBasic() new R2RTestRunner().Run(testCase); } + + /// + /// PR #124203: Async methods produce [ASYNC] variant entries with resumption stubs. + /// PR #121456: Resumption stubs are emitted as ResumptionStubEntryPoint fixups. + /// PR #123643: Methods with GC refs across awaits produce ContinuationLayout fixups. + /// + [Fact] + public void RuntimeAsyncMethodEmission() + { + string attrSource = R2RTestCaseCompiler.ReadEmbeddedSource( + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"); + string mainSource = R2RTestCaseCompiler.ReadEmbeddedSource( + "RuntimeAsync/BasicAsyncEmission.cs"); + + var expectations = new R2RExpectations(); + expectations.Features.Add(RuntimeAsyncFeature); + expectations.ExpectedAsyncVariantMethods.Add("SimpleAsyncMethod"); + expectations.ExpectedAsyncVariantMethods.Add("AsyncVoidReturn"); + expectations.ExpectedAsyncVariantMethods.Add("ValueTaskMethod"); + + var testCase = new R2RTestCase + { + Name = "RuntimeAsyncMethodEmission", + MainSourceResourceName = "RuntimeAsync/BasicAsyncEmission.cs", + MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, + Dependencies = new List(), + Expectations = expectations, + }; + + new R2RTestRunner().Run(testCase); + } + + /// + /// PR #123643: Async methods capturing GC refs across await points + /// produce ContinuationLayout fixups encoding the GC ref map. + /// PR #124203: Resumption stubs for methods with suspension points. + /// + [Fact] + public void RuntimeAsyncContinuationLayout() + { + string attrSource = R2RTestCaseCompiler.ReadEmbeddedSource( + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"); + string mainSource = R2RTestCaseCompiler.ReadEmbeddedSource( + "RuntimeAsync/AsyncWithContinuation.cs"); + + var expectations = new R2RExpectations + { + ExpectContinuationLayout = true, + ExpectResumptionStubFixup = true, + }; + expectations.Features.Add(RuntimeAsyncFeature); + expectations.ExpectedAsyncVariantMethods.Add("CaptureObjectAcrossAwait"); + expectations.ExpectedAsyncVariantMethods.Add("CaptureMultipleRefsAcrossAwait"); + + var testCase = new R2RTestCase + { + Name = "RuntimeAsyncContinuationLayout", + MainSourceResourceName = "RuntimeAsync/AsyncWithContinuation.cs", + MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, + Dependencies = new List(), + Expectations = expectations, + }; + + new R2RTestRunner().Run(testCase); + } + + /// + /// PR #125420: Devirtualization of async methods through + /// AsyncAwareVirtualMethodResolutionAlgorithm. + /// + [Fact] + public void RuntimeAsyncDevirtualize() + { + var expectations = new R2RExpectations(); + expectations.Features.Add(RuntimeAsyncFeature); + expectations.ExpectedAsyncVariantMethods.Add("GetValueAsync"); + + var testCase = new R2RTestCase + { + Name = "RuntimeAsyncDevirtualize", + MainSourceResourceName = "RuntimeAsync/AsyncDevirtualize.cs", + MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, + Dependencies = new List(), + Expectations = expectations, + }; + + new R2RTestRunner().Run(testCase); + } + + /// + /// PR #124203: Async methods without yield points may omit resumption stubs. + /// Validates that no-yield async methods still produce [ASYNC] variants. + /// + [Fact] + public void RuntimeAsyncNoYield() + { + var expectations = new R2RExpectations(); + expectations.Features.Add(RuntimeAsyncFeature); + expectations.ExpectedAsyncVariantMethods.Add("AsyncButNoAwait"); + expectations.ExpectedAsyncVariantMethods.Add("AsyncWithConditionalAwait"); + + var testCase = new R2RTestCase + { + Name = "RuntimeAsyncNoYield", + MainSourceResourceName = "RuntimeAsync/AsyncNoYield.cs", + MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, + Dependencies = new List(), + Expectations = expectations, + }; + + new R2RTestRunner().Run(testCase); + } + + /// + /// PR #121679: MutableModule async references + cross-module inlining + /// of runtime-async methods with cross-module dependency. + /// + [Fact] + public void RuntimeAsyncCrossModule() + { + var expectations = new R2RExpectations(); + expectations.Features.Add(RuntimeAsyncFeature); + expectations.ExpectedManifestRefs.Add("AsyncDepLib"); + expectations.ExpectedAsyncVariantMethods.Add("CallCrossModuleAsync"); + expectations.Crossgen2Options.Add("--opt-cross-module:AsyncDepLib"); + + var testCase = new R2RTestCase + { + Name = "RuntimeAsyncCrossModule", + MainSourceResourceName = "RuntimeAsync/AsyncCrossModule.cs", + MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, + Dependencies = new List + { + new DependencyInfo + { + AssemblyName = "AsyncDepLib", + SourceResourceNames = new[] + { + "RuntimeAsync/Dependencies/AsyncDepLib.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" + }, + Crossgen = true, + Features = { RuntimeAsyncFeature }, + } + }, + Expectations = expectations, + }; + + new R2RTestRunner().Run(testCase); + } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs new file mode 100644 index 00000000000000..28534b5a9fcf16 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs @@ -0,0 +1,28 @@ +// Test: Cross-module async method inlining +// Validates that async methods from a dependency library can be +// cross-module inlined, creating manifest refs and CHECK_IL_BODY fixups. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncCrossModule +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCrossModuleAsync() + { + return await AsyncDepLib.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCrossModuleStringAsync() + { + return await AsyncDepLib.GetStringAsync(); + } + + // Call a non-async sync method from async lib + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CallCrossModuleSync() + { + return AsyncDepLib.GetValueSync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs new file mode 100644 index 00000000000000..76d69eecf2fc3f --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs @@ -0,0 +1,56 @@ +// Test: Async virtual method devirtualization in R2R +// Validates that sealed class and interface dispatch of async methods +// produces devirtualized direct call entries in the R2R image. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public interface IAsyncService +{ + Task GetValueAsync(); +} + +public class OpenImpl : IAsyncService +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public virtual async Task GetValueAsync() + { + await Task.Yield(); + return 10; + } +} + +public sealed class SealedImpl : IAsyncService +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task GetValueAsync() + { + await Task.Yield(); + return 20; + } +} + +public static class AsyncDevirtualize +{ + // Sealed type known at compile time — should devirtualize + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallOnSealed(SealedImpl obj) + { + return await obj.GetValueAsync(); + } + + // newobj gives exact type info — should devirtualize through resolveVirtualMethod + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallOnNewOpen() + { + IAsyncService svc = new OpenImpl(); + return await svc.GetValueAsync(); + } + + // Generic constrained dispatch + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallGenericConstrained(T obj) where T : IAsyncService + { + return await obj.GetValueAsync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs new file mode 100644 index 00000000000000..28ddb07949521c --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs @@ -0,0 +1,23 @@ +// Test: Async method without yields (no suspension point) +// When a runtime-async method never actually awaits, crossgen2 may +// omit the resumption stub. This tests that edge case. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncNoYield +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task AsyncButNoAwait() + { + return 42; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task AsyncWithConditionalAwait(bool doAwait) + { + if (doAwait) + await Task.Yield(); + return 1; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs new file mode 100644 index 00000000000000..29a8fc879ec463 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs @@ -0,0 +1,26 @@ +// Test: Async method that captures GC refs across await +// This forces the compiler to emit a ContinuationLayout fixup. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncWithContinuation +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CaptureObjectAcrossAwait() + { + object o = new object(); + string s = "hello"; + await Task.Yield(); + return s + o.GetHashCode(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CaptureMultipleRefsAcrossAwait() + { + int[] arr = new int[] { 1, 2, 3 }; + string text = "world"; + await Task.Yield(); + return arr[0] + text.Length; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs new file mode 100644 index 00000000000000..c6137cfc309188 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs @@ -0,0 +1,36 @@ +// Test: Basic async method emission in R2R +// Validates that runtime-async methods produce [ASYNC] variant entries and +// resumption stubs in the R2R image. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class BasicAsyncEmission +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task SimpleAsyncMethod() + { + await Task.Yield(); + return 42; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task AsyncVoidReturn() + { + await Task.Yield(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async ValueTask ValueTaskMethod() + { + await Task.Yield(); + return "hello"; + } + + // Non-async method that returns Task (no await) — should NOT get async variant + [MethodImpl(MethodImplOptions.NoInlining)] + public static Task SyncTaskReturning() + { + return Task.FromResult(1); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs new file mode 100644 index 00000000000000..bb10453f70b05e --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs @@ -0,0 +1,28 @@ +// Dependency library for async cross-module tests. +// Contains runtime-async methods that should be inlineable. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncDepLib +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetValueAsync() + { + await Task.Yield(); + return 42; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetStringAsync() + { + await Task.Yield(); + return "async_hello"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetValueSync() + { + return 99; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs new file mode 100644 index 00000000000000..e481c3bcefd748 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Method)] +internal class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribute +{ + public bool RuntimeAsync { get; } = runtimeAsync; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index d7694a3e1998e1..e0565ad1f07f5e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -22,6 +22,30 @@ internal sealed class R2RExpectations public List ExpectedInlinedMethods { get; } = new(); public bool CompositeMode { get; set; } public List Crossgen2Options { get; } = new(); + /// + /// Roslyn feature flags for the main assembly (e.g. runtime-async=on). + /// + public List> Features { get; } = new(); + /// + /// Method names expected to have [ASYNC] variant entries in the R2R image. + /// + public List ExpectedAsyncVariantMethods { get; } = new(); + /// + /// Method names expected to have [RESUME] (resumption stub) entries. + /// + public List ExpectedResumptionStubs { get; } = new(); + /// + /// If true, expect at least one ContinuationLayout fixup in the image. + /// + public bool ExpectContinuationLayout { get; set; } + /// + /// If true, expect at least one ResumptionStubEntryPoint fixup in the image. + /// + public bool ExpectResumptionStubFixup { get; set; } + /// + /// Fixup kinds that must be present somewhere in the image. + /// + public List ExpectedFixupKinds { get; } = new(); } internal sealed record ExpectedInlinedMethod(string MethodName); @@ -48,6 +72,9 @@ public void Check(string r2rImagePath, R2RExpectations expectations) CheckManifestRefs(reader, expectations, r2rImagePath); CheckInlinedMethods(reader, expectations, r2rImagePath); + CheckAsyncVariantMethods(reader, expectations, r2rImagePath); + CheckResumptionStubs(reader, expectations, r2rImagePath); + CheckFixupKinds(reader, expectations, r2rImagePath); } private static void CheckManifestRefs(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) @@ -128,6 +155,107 @@ void CollectFixups(ReadyToRunMethod method) $"All fixups: [{string.Join("; ", allFixupSummary)}]"); } } + + private static List GetAllMethods(ReadyToRunReader reader) + { + var methods = new List(); + foreach (var assembly in reader.ReadyToRunAssemblies) + methods.AddRange(assembly.Methods); + foreach (var instanceMethod in reader.InstanceMethods) + methods.Add(instanceMethod.Method); + + return methods; + } + + private static void CheckAsyncVariantMethods(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + { + if (expectations.ExpectedAsyncVariantMethods.Count == 0) + return; + + var allMethods = GetAllMethods(reader); + var asyncMethods = allMethods + .Where(m => m.SignatureString.Contains("[ASYNC]", StringComparison.OrdinalIgnoreCase)) + .Select(m => m.SignatureString) + .ToList(); + + foreach (string expected in expectations.ExpectedAsyncVariantMethods) + { + bool found = asyncMethods.Any(sig => + sig.Contains(expected, StringComparison.OrdinalIgnoreCase)); + + Assert.True(found, + $"Expected [ASYNC] variant for '{expected}' not found in '{Path.GetFileName(imagePath)}'. " + + $"Async methods: [{string.Join(", ", asyncMethods)}]. " + + $"All methods: [{string.Join(", ", allMethods.Select(m => m.SignatureString).Take(30))}]"); + } + } + + private static void CheckResumptionStubs(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + { + if (expectations.ExpectedResumptionStubs.Count == 0 && !expectations.ExpectResumptionStubFixup) + return; + + var allMethods = GetAllMethods(reader); + var resumeMethods = allMethods + .Where(m => m.SignatureString.Contains("[RESUME]", StringComparison.OrdinalIgnoreCase)) + .Select(m => m.SignatureString) + .ToList(); + + foreach (string expected in expectations.ExpectedResumptionStubs) + { + bool found = resumeMethods.Any(sig => + sig.Contains(expected, StringComparison.OrdinalIgnoreCase)); + + Assert.True(found, + $"Expected [RESUME] stub for '{expected}' not found in '{Path.GetFileName(imagePath)}'. " + + $"Resume methods: [{string.Join(", ", resumeMethods)}]. " + + $"All methods: [{string.Join(", ", allMethods.Select(m => m.SignatureString).Take(30))}]"); + } + + if (expectations.ExpectResumptionStubFixup) + { + var formattingOptions = new SignatureFormattingOptions(); + bool hasResumptionFixup = allMethods.Any(m => + m.Fixups.Any(c => + c.Signature?.FixupKind == ReadyToRunFixupKind.ResumptionStubEntryPoint)); + + Assert.True(hasResumptionFixup, + $"Expected ResumptionStubEntryPoint fixup not found in '{Path.GetFileName(imagePath)}'."); + } + } + + private static void CheckFixupKinds(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + { + if (expectations.ExpectedFixupKinds.Count == 0 && !expectations.ExpectContinuationLayout) + return; + + var allMethods = GetAllMethods(reader); + var presentKinds = new HashSet(); + foreach (var method in allMethods) + { + if (method.Fixups is null) + continue; + foreach (var cell in method.Fixups) + { + if (cell.Signature is not null) + presentKinds.Add(cell.Signature.FixupKind); + } + } + + if (expectations.ExpectContinuationLayout) + { + Assert.True(presentKinds.Contains(ReadyToRunFixupKind.ContinuationLayout), + $"Expected ContinuationLayout fixup not found in '{Path.GetFileName(imagePath)}'. " + + $"Present fixup kinds: [{string.Join(", ", presentKinds)}]"); + } + + foreach (var expectedKind in expectations.ExpectedFixupKinds) + { + Assert.True(presentKinds.Contains(expectedKind), + $"Expected fixup kind '{expectedKind}' not found in '{Path.GetFileName(imagePath)}'. " + + $"Present fixup kinds: [{string.Join(", ", presentKinds)}]"); + } + } } /// diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs index 8cbbc439d6cfb8..c92c52d16c58af 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs @@ -53,18 +53,25 @@ public R2RTestCaseCompiler(string outputDir) /// Paths to additional assembly references. /// Library or ConsoleApplication. /// Additional preprocessor defines. + /// Roslyn feature flags (e.g. "runtime-async=on"). /// Path to the compiled assembly. public string CompileAssembly( string assemblyName, IEnumerable sources, IEnumerable? additionalReferences = null, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, - IEnumerable? additionalDefines = null) + IEnumerable? additionalDefines = null, + IEnumerable>? features = null) { + var parseOptions = new CSharpParseOptions( + LanguageVersion.Latest, + preprocessorSymbols: additionalDefines); + + if (features is not null) + parseOptions = parseOptions.WithFeatures(features); + var syntaxTrees = sources.Select(src => - CSharpSyntaxTree.ParseText(src, new CSharpParseOptions( - LanguageVersion.Latest, - preprocessorSymbols: additionalDefines))); + CSharpSyntaxTree.ParseText(src, parseOptions)); var references = new List(_frameworkReferences); if (additionalReferences is not null) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs index 1c372ce8c3176c..22131fe08acf87 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs @@ -17,6 +17,10 @@ internal sealed class R2RTestCase { public required string Name { get; init; } public required string MainSourceResourceName { get; init; } + /// + /// Additional source files to compile with the main assembly (e.g. shared attribute files). + /// + public string[]? MainExtraSourceResourceNames { get; init; } public required List Dependencies { get; init; } public required R2RExpectations Expectations { get; init; } } @@ -31,6 +35,10 @@ internal sealed class DependencyInfo public bool Crossgen { get; init; } public List CrossgenOptions { get; init; } = new(); public List AdditionalReferences { get; init; } = new(); + /// + /// Roslyn feature flags for this dependency (e.g. runtime-async=on). + /// + public List> Features { get; init; } = new(); } /// @@ -66,15 +74,26 @@ public void Run(R2RTestCase testCase) .Select(r => compiledDeps.First(d => d.Dep.AssemblyName == r).IlPath) .ToList(); - string ilPath = compiler.CompileAssembly(dep.AssemblyName, sources, refs); + string ilPath = compiler.CompileAssembly(dep.AssemblyName, sources, refs, + features: dep.Features.Count > 0 ? dep.Features : null); compiledDeps.Add((dep, ilPath)); } // Step 2: Compile main assembly with Roslyn - string mainSource = R2RTestCaseCompiler.ReadEmbeddedSource(testCase.MainSourceResourceName); + var mainSources = new List + { + R2RTestCaseCompiler.ReadEmbeddedSource(testCase.MainSourceResourceName) + }; + if (testCase.MainExtraSourceResourceNames is not null) + { + foreach (string extra in testCase.MainExtraSourceResourceNames) + mainSources.Add(R2RTestCaseCompiler.ReadEmbeddedSource(extra)); + } + var mainRefs = compiledDeps.Select(d => d.IlPath).ToList(); - string mainIlPath = compiler.CompileAssembly(testCase.Name, new[] { mainSource }, mainRefs, - outputKind: Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary); + string mainIlPath = compiler.CompileAssembly(testCase.Name, mainSources, mainRefs, + outputKind: Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary, + features: testCase.Expectations.Features.Count > 0 ? testCase.Expectations.Features : null); // Step 3: Crossgen2 dependencies var driver = new R2RDriver(); From 7bb35e58d85f2c4ae0a84c700f8fcbd5937fe502 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:02:14 -0700 Subject: [PATCH 14/74] Refactor R2R test model: rename DependencyInfo to CompiledAssembly, add typed Crossgen2Option - Rename DependencyInfo -> CompiledAssembly with IsCrossgenInput property - Remove AdditionalReferences: compile dependencies in listed order, each referencing all previously compiled assemblies - Add Crossgen2OptionKind enum and Crossgen2Option record replacing raw string CLI args - Remove unused CrossgenOptions from CompiledAssembly - Remove dead ReadEmbeddedSource locals in async tests - Fix Crossgen2Dir fallback to check for crossgen2.dll file instead of just directory existence (empty Debug dir was preventing Checked fallback) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 61 ++++++++----------- .../TestCasesRunner/R2RDriver.cs | 24 ++++++++ .../TestCasesRunner/R2RResultChecker.cs | 2 +- .../TestCasesRunner/R2RTestRunner.cs | 35 +++++------ .../TestCasesRunner/TestPaths.cs | 4 +- 5 files changed, 70 insertions(+), 56 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 620c7da3dc1766..8f72969598a97f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -23,19 +23,19 @@ public void BasicCrossModuleInlining() expectations.ExpectedManifestRefs.Add("InlineableLib"); expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetValue")); expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetString")); - expectations.Crossgen2Options.Add("--opt-cross-module:InlineableLib"); + expectations.Crossgen2Options.Add(Crossgen2Option.CrossModuleOptimization("InlineableLib")); var testCase = new R2RTestCase { Name = "BasicCrossModuleInlining", MainSourceResourceName = "CrossModuleInlining/BasicInlining.cs", - Dependencies = new List + Dependencies = new List { - new DependencyInfo + new CompiledAssembly { AssemblyName = "InlineableLib", SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/InlineableLib.cs" }, - Crossgen = true, + IsCrossgenInput = true, } }, Expectations = expectations, @@ -51,26 +51,25 @@ public void TransitiveReferences() expectations.ExpectedManifestRefs.Add("InlineableLibTransitive"); expectations.ExpectedManifestRefs.Add("ExternalLib"); expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetExternalValue")); - expectations.Crossgen2Options.Add("--opt-cross-module:InlineableLibTransitive"); + expectations.Crossgen2Options.Add(Crossgen2Option.CrossModuleOptimization("InlineableLibTransitive")); var testCase = new R2RTestCase { Name = "TransitiveReferences", MainSourceResourceName = "CrossModuleInlining/TransitiveReferences.cs", - Dependencies = new List + Dependencies = new List { - new DependencyInfo + new CompiledAssembly { AssemblyName = "ExternalLib", SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/ExternalLib.cs" }, - Crossgen = false, + IsCrossgenInput = false, }, - new DependencyInfo + new CompiledAssembly { AssemblyName = "InlineableLibTransitive", SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/InlineableLibTransitive.cs" }, - Crossgen = true, - AdditionalReferences = { "ExternalLib" }, + IsCrossgenInput = true, } }, Expectations = expectations, @@ -85,19 +84,19 @@ public void AsyncCrossModuleInlining() var expectations = new R2RExpectations(); expectations.ExpectedManifestRefs.Add("AsyncInlineableLib"); expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetValueAsync")); - expectations.Crossgen2Options.Add("--opt-cross-module:AsyncInlineableLib"); + expectations.Crossgen2Options.Add(Crossgen2Option.CrossModuleOptimization("AsyncInlineableLib")); var testCase = new R2RTestCase { Name = "AsyncCrossModuleInlining", MainSourceResourceName = "CrossModuleInlining/AsyncMethods.cs", - Dependencies = new List + Dependencies = new List { - new DependencyInfo + new CompiledAssembly { AssemblyName = "AsyncInlineableLib", SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/AsyncInlineableLib.cs" }, - Crossgen = true, + IsCrossgenInput = true, } }, Expectations = expectations, @@ -119,13 +118,13 @@ public void CompositeBasic() { Name = "CompositeBasic", MainSourceResourceName = "CrossModuleInlining/CompositeBasic.cs", - Dependencies = new List + Dependencies = new List { - new DependencyInfo + new CompiledAssembly { AssemblyName = "CompositeLib", SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/CompositeLib.cs" }, - Crossgen = true, + IsCrossgenInput = true, } }, Expectations = expectations, @@ -142,11 +141,6 @@ public void CompositeBasic() [Fact] public void RuntimeAsyncMethodEmission() { - string attrSource = R2RTestCaseCompiler.ReadEmbeddedSource( - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"); - string mainSource = R2RTestCaseCompiler.ReadEmbeddedSource( - "RuntimeAsync/BasicAsyncEmission.cs"); - var expectations = new R2RExpectations(); expectations.Features.Add(RuntimeAsyncFeature); expectations.ExpectedAsyncVariantMethods.Add("SimpleAsyncMethod"); @@ -158,7 +152,7 @@ public void RuntimeAsyncMethodEmission() Name = "RuntimeAsyncMethodEmission", MainSourceResourceName = "RuntimeAsync/BasicAsyncEmission.cs", MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, - Dependencies = new List(), + Dependencies = new List(), Expectations = expectations, }; @@ -173,11 +167,6 @@ public void RuntimeAsyncMethodEmission() [Fact] public void RuntimeAsyncContinuationLayout() { - string attrSource = R2RTestCaseCompiler.ReadEmbeddedSource( - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"); - string mainSource = R2RTestCaseCompiler.ReadEmbeddedSource( - "RuntimeAsync/AsyncWithContinuation.cs"); - var expectations = new R2RExpectations { ExpectContinuationLayout = true, @@ -192,7 +181,7 @@ public void RuntimeAsyncContinuationLayout() Name = "RuntimeAsyncContinuationLayout", MainSourceResourceName = "RuntimeAsync/AsyncWithContinuation.cs", MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, - Dependencies = new List(), + Dependencies = new List(), Expectations = expectations, }; @@ -215,7 +204,7 @@ public void RuntimeAsyncDevirtualize() Name = "RuntimeAsyncDevirtualize", MainSourceResourceName = "RuntimeAsync/AsyncDevirtualize.cs", MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, - Dependencies = new List(), + Dependencies = new List(), Expectations = expectations, }; @@ -239,7 +228,7 @@ public void RuntimeAsyncNoYield() Name = "RuntimeAsyncNoYield", MainSourceResourceName = "RuntimeAsync/AsyncNoYield.cs", MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, - Dependencies = new List(), + Dependencies = new List(), Expectations = expectations, }; @@ -257,16 +246,16 @@ public void RuntimeAsyncCrossModule() expectations.Features.Add(RuntimeAsyncFeature); expectations.ExpectedManifestRefs.Add("AsyncDepLib"); expectations.ExpectedAsyncVariantMethods.Add("CallCrossModuleAsync"); - expectations.Crossgen2Options.Add("--opt-cross-module:AsyncDepLib"); + expectations.Crossgen2Options.Add(Crossgen2Option.CrossModuleOptimization("AsyncDepLib")); var testCase = new R2RTestCase { Name = "RuntimeAsyncCrossModule", MainSourceResourceName = "RuntimeAsync/AsyncCrossModule.cs", MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, - Dependencies = new List + Dependencies = new List { - new DependencyInfo + new CompiledAssembly { AssemblyName = "AsyncDepLib", SourceResourceNames = new[] @@ -274,7 +263,7 @@ public void RuntimeAsyncCrossModule() "RuntimeAsync/Dependencies/AsyncDepLib.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, - Crossgen = true, + IsCrossgenInput = true, Features = { RuntimeAsyncFeature }, } }, diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs index 6aa4f9d3479cbb..5eb4d40719d90a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs @@ -10,6 +10,30 @@ namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; +/// +/// Known crossgen2 option kinds. +/// +internal enum Crossgen2OptionKind +{ + /// Enables cross-module inlining for a named assembly (--opt-cross-module:AssemblyName). + CrossModuleOptimization, +} + +/// +/// A typed crossgen2 option with optional parameter value. +/// +internal sealed record Crossgen2Option(Crossgen2OptionKind Kind, string? Value = null) +{ + public static Crossgen2Option CrossModuleOptimization(string assemblyName) + => new(Crossgen2OptionKind.CrossModuleOptimization, assemblyName); + + public IEnumerable ToArgs() => Kind switch + { + Crossgen2OptionKind.CrossModuleOptimization => [$"--opt-cross-module:{Value}"], + _ => throw new ArgumentOutOfRangeException(nameof(Kind)), + }; +} + /// /// Result of a crossgen2 compilation step. /// diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index e0565ad1f07f5e..23f7d341591640 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -21,7 +21,7 @@ internal sealed class R2RExpectations public List ExpectedManifestRefs { get; } = new(); public List ExpectedInlinedMethods { get; } = new(); public bool CompositeMode { get; set; } - public List Crossgen2Options { get; } = new(); + public List Crossgen2Options { get; } = new(); /// /// Roslyn feature flags for the main assembly (e.g. runtime-async=on). /// diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs index 22131fe08acf87..fcafd2860df3a5 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs @@ -21,20 +21,23 @@ internal sealed class R2RTestCase /// Additional source files to compile with the main assembly (e.g. shared attribute files). /// public string[]? MainExtraSourceResourceNames { get; init; } - public required List Dependencies { get; init; } + public required List Dependencies { get; init; } public required R2RExpectations Expectations { get; init; } } /// -/// Describes a dependency assembly for a test case. +/// Describes an assembly compiled as part of a test case. +/// Dependencies are compiled in listed order — each assembly can reference all previously compiled assemblies. /// -internal sealed class DependencyInfo +internal sealed class CompiledAssembly { public required string AssemblyName { get; init; } public required string[] SourceResourceNames { get; init; } - public bool Crossgen { get; init; } - public List CrossgenOptions { get; init; } = new(); - public List AdditionalReferences { get; init; } = new(); + /// + /// If true, this assembly is passed as an input to crossgen2. + /// If false, it is only used as a reference (--ref) during crossgen2 compilation. + /// + public bool IsCrossgenInput { get; init; } /// /// Roslyn feature flags for this dependency (e.g. runtime-async=on). /// @@ -60,9 +63,9 @@ public void Run(R2RTestCase testCase) Directory.CreateDirectory(ilDir); Directory.CreateDirectory(r2rDir); - // Step 1: Compile all dependencies with Roslyn + // Step 1: Compile all dependencies with Roslyn (in order, leaf to root) var compiler = new R2RTestCaseCompiler(ilDir); - var compiledDeps = new List<(DependencyInfo Dep, string IlPath)>(); + var compiledDeps = new List<(CompiledAssembly Dep, string IlPath)>(); foreach (var dep in testCase.Dependencies) { @@ -70,9 +73,8 @@ public void Run(R2RTestCase testCase) .Select(R2RTestCaseCompiler.ReadEmbeddedSource) .ToList(); - var refs = dep.AdditionalReferences - .Select(r => compiledDeps.First(d => d.Dep.AssemblyName == r).IlPath) - .ToList(); + // Each dependency can reference all previously compiled assemblies + var refs = compiledDeps.Select(d => d.IlPath).ToList(); string ilPath = compiler.CompileAssembly(dep.AssemblyName, sources, refs, features: dep.Features.Count > 0 ? dep.Features : null); @@ -101,7 +103,7 @@ public void Run(R2RTestCase testCase) foreach (var (dep, ilPath) in compiledDeps) { - if (!dep.Crossgen) + if (!dep.IsCrossgenInput) continue; string r2rPath = Path.Combine(r2rDir, Path.GetFileName(ilPath)); @@ -110,7 +112,6 @@ public void Run(R2RTestCase testCase) InputPath = ilPath, OutputPath = r2rPath, ReferencePaths = allRefPaths, - ExtraArgs = dep.CrossgenOptions, }); Assert.True(result.Success, @@ -156,7 +157,7 @@ private static void RunSingleCompilation( InputPath = mainIlPath, OutputPath = mainR2RPath, ReferencePaths = allRefPaths, - ExtraArgs = testCase.Expectations.Crossgen2Options.ToList(), + ExtraArgs = testCase.Expectations.Crossgen2Options.SelectMany(o => o.ToArgs()).ToList(), }; var result = driver.Compile(options); @@ -172,12 +173,12 @@ private static void RunCompositeCompilation( string mainIlPath, string mainR2RPath, List allRefPaths, - List<(DependencyInfo Dep, string IlPath)> compiledDeps) + List<(CompiledAssembly Dep, string IlPath)> compiledDeps) { var compositeInputs = new List { mainIlPath }; foreach (var (dep, ilPath) in compiledDeps) { - if (dep.Crossgen) + if (dep.IsCrossgenInput) compositeInputs.Add(ilPath); } @@ -188,7 +189,7 @@ private static void RunCompositeCompilation( ReferencePaths = allRefPaths, Composite = true, CompositeInputPaths = compositeInputs, - ExtraArgs = testCase.Expectations.Crossgen2Options.ToList(), + ExtraArgs = testCase.Expectations.Crossgen2Options.SelectMany(o => o.ToArgs()).ToList(), }; var result = driver.Compile(options); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs index 2524ae4fd867fd..05bc828f18d98e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs @@ -31,14 +31,14 @@ public static string Crossgen2Dir get { string dir = GetRequiredConfig("R2RTest.Crossgen2Dir"); - if (!Directory.Exists(dir)) + if (!File.Exists(Path.Combine(dir, "crossgen2.dll"))) { // Try Checked and Release fallbacks since crossgen2 may be built in a different config foreach (string fallbackConfig in new[] { "Checked", "Release", "Debug" }) { string fallback = Regex.Replace( dir, @"\.(Debug|Release|Checked)[/\\]", $".{fallbackConfig}/"); - if (Directory.Exists(fallback)) + if (File.Exists(Path.Combine(fallback, "crossgen2.dll"))) return fallback; } } From 804ec5551716c6c9a622895ea5eac904ebb063f7 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:13:08 -0700 Subject: [PATCH 15/74] Replace R2RExpectations with Action validation callback - R2RTestCase now takes an Action Validate callback instead of an R2RExpectations data object - Move CompositeMode, Crossgen2Options, Features to R2RTestCase directly - Convert R2RResultChecker to R2RAssert static helper class with methods: HasManifestRef, HasInlinedMethod, HasAsyncVariant, HasResumptionStub, HasContinuationLayout, HasResumptionStubFixup, HasFixupKind - Delete unused Expectations/R2RExpectationAttributes.cs (attribute-based design replaced by inline test code) - Each test now owns its assertions directly, making them more readable and easier to extend with custom checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Expectations/R2RExpectationAttributes.cs | 123 --------- .../TestCases/R2RTestSuites.cs | 213 +++++++-------- .../TestCasesRunner/R2RResultChecker.cs | 258 +++++------------- .../TestCasesRunner/R2RTestRunner.cs | 39 ++- 4 files changed, 196 insertions(+), 437 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs deleted file mode 100644 index 8fe039762c1ce7..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Diagnostics; - -namespace ILCompiler.ReadyToRun.Tests.Expectations; - -/// -/// Marks a method as expected to be cross-module inlined into the main R2R image. -/// The R2R result checker will verify a CHECK_IL_BODY fixup exists for this method's callee. -/// -[Conditional("R2R_EXPECTATIONS")] -[AttributeUsage(AttributeTargets.Method)] -public sealed class ExpectInlinedAttribute : Attribute -{ - /// - /// The fully qualified name of the method expected to be inlined. - /// If null, infers from the method body (looks for the first cross-module call). - /// - public string? MethodName { get; set; } -} - -/// -/// Marks a method as expected to have a specific number of RuntimeFunctions in the R2R image. -/// -[Conditional("R2R_EXPECTATIONS")] -[AttributeUsage(AttributeTargets.Method)] -public sealed class ExpectRuntimeFunctionCountAttribute : Attribute -{ - public int ExpectedCount { get; set; } -} - -/// -/// Declares that the R2R image should contain a manifest reference to the specified assembly. -/// -[Conditional("R2R_EXPECTATIONS")] -[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] -public sealed class ExpectManifestRefAttribute : Attribute -{ - public string AssemblyName { get; } - - public ExpectManifestRefAttribute(string assemblyName) - { - AssemblyName = assemblyName; - } -} - -/// -/// Specifies a crossgen2 command-line option for the main assembly compilation. -/// Applied at the assembly level of the test case. -/// -[Conditional("R2R_EXPECTATIONS")] -[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] -public sealed class Crossgen2OptionAttribute : Attribute -{ - public string Option { get; } - - public Crossgen2OptionAttribute(string option) - { - Option = option; - } -} - -/// -/// Declares a dependency assembly that should be compiled before the main test assembly. -/// The source files are compiled with Roslyn, then optionally crossgen2'd before the main assembly. -/// -[Conditional("R2R_EXPECTATIONS")] -[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] -public sealed class SetupCompileBeforeAttribute : Attribute -{ - /// - /// The output assembly filename (e.g., "InlineableLib.dll"). - /// - public string OutputName { get; } - - /// - /// Source file paths relative to the test case's Dependencies/ folder. - /// - public string[] SourceFiles { get; } - - /// - /// Additional assembly references needed to compile this dependency. - /// - public string[]? References { get; set; } - - /// - /// If true, this assembly is also crossgen2'd before the main assembly. - /// - public bool Crossgen { get; set; } - - /// - /// Additional crossgen2 options for this dependency assembly. - /// - public string[]? CrossgenOptions { get; set; } - - public SetupCompileBeforeAttribute(string outputName, string[] sourceFiles) - { - OutputName = outputName; - SourceFiles = sourceFiles; - } -} - -/// -/// Marks a method as expected to have R2R compiled code in the output image. -/// -[Conditional("R2R_EXPECTATIONS")] -[AttributeUsage(AttributeTargets.Method)] -public sealed class ExpectR2RMethodAttribute : Attribute -{ -} - -/// -/// Marks an assembly-level option to enable composite mode compilation. -/// When present, all SetupCompileBefore assemblies with Crossgen=true are compiled -/// together with the main assembly using --composite. -/// -[Conditional("R2R_EXPECTATIONS")] -[AttributeUsage(AttributeTargets.Assembly)] -public sealed class CompositeModeAttribute : Attribute -{ -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 8f72969598a97f..7fdd8d422af7a9 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; -using Internal.ReadyToRunConstants; using Xunit; namespace ILCompiler.ReadyToRun.Tests.TestCases; @@ -19,118 +18,108 @@ public class R2RTestSuites [Fact] public void BasicCrossModuleInlining() { - var expectations = new R2RExpectations(); - expectations.ExpectedManifestRefs.Add("InlineableLib"); - expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetValue")); - expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetString")); - expectations.Crossgen2Options.Add(Crossgen2Option.CrossModuleOptimization("InlineableLib")); - - var testCase = new R2RTestCase + new R2RTestRunner().Run(new R2RTestCase { Name = "BasicCrossModuleInlining", MainSourceResourceName = "CrossModuleInlining/BasicInlining.cs", + Crossgen2Options = { Crossgen2Option.CrossModuleOptimization("InlineableLib") }, Dependencies = new List { - new CompiledAssembly + new() { AssemblyName = "InlineableLib", - SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/InlineableLib.cs" }, + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], IsCrossgenInput = true, } }, - Expectations = expectations, - }; - - new R2RTestRunner().Run(testCase); + Validate = reader => + { + R2RAssert.HasManifestRef(reader, "InlineableLib"); + R2RAssert.HasInlinedMethod(reader, "GetValue"); + R2RAssert.HasInlinedMethod(reader, "GetString"); + }, + }); } [Fact] public void TransitiveReferences() { - var expectations = new R2RExpectations(); - expectations.ExpectedManifestRefs.Add("InlineableLibTransitive"); - expectations.ExpectedManifestRefs.Add("ExternalLib"); - expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetExternalValue")); - expectations.Crossgen2Options.Add(Crossgen2Option.CrossModuleOptimization("InlineableLibTransitive")); - - var testCase = new R2RTestCase + new R2RTestRunner().Run(new R2RTestCase { Name = "TransitiveReferences", MainSourceResourceName = "CrossModuleInlining/TransitiveReferences.cs", + Crossgen2Options = { Crossgen2Option.CrossModuleOptimization("InlineableLibTransitive") }, Dependencies = new List { - new CompiledAssembly + new() { AssemblyName = "ExternalLib", - SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/ExternalLib.cs" }, + SourceResourceNames = ["CrossModuleInlining/Dependencies/ExternalLib.cs"], IsCrossgenInput = false, }, - new CompiledAssembly + new() { AssemblyName = "InlineableLibTransitive", - SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/InlineableLibTransitive.cs" }, + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLibTransitive.cs"], IsCrossgenInput = true, } }, - Expectations = expectations, - }; - - new R2RTestRunner().Run(testCase); + Validate = reader => + { + R2RAssert.HasManifestRef(reader, "InlineableLibTransitive"); + R2RAssert.HasManifestRef(reader, "ExternalLib"); + R2RAssert.HasInlinedMethod(reader, "GetExternalValue"); + }, + }); } [Fact] public void AsyncCrossModuleInlining() { - var expectations = new R2RExpectations(); - expectations.ExpectedManifestRefs.Add("AsyncInlineableLib"); - expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetValueAsync")); - expectations.Crossgen2Options.Add(Crossgen2Option.CrossModuleOptimization("AsyncInlineableLib")); - - var testCase = new R2RTestCase + new R2RTestRunner().Run(new R2RTestCase { Name = "AsyncCrossModuleInlining", MainSourceResourceName = "CrossModuleInlining/AsyncMethods.cs", + Crossgen2Options = { Crossgen2Option.CrossModuleOptimization("AsyncInlineableLib") }, Dependencies = new List { - new CompiledAssembly + new() { AssemblyName = "AsyncInlineableLib", - SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/AsyncInlineableLib.cs" }, + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncInlineableLib.cs"], IsCrossgenInput = true, } }, - Expectations = expectations, - }; - - new R2RTestRunner().Run(testCase); + Validate = reader => + { + R2RAssert.HasManifestRef(reader, "AsyncInlineableLib"); + R2RAssert.HasInlinedMethod(reader, "GetValueAsync"); + }, + }); } [Fact] public void CompositeBasic() { - var expectations = new R2RExpectations - { - CompositeMode = true, - }; - expectations.ExpectedManifestRefs.Add("CompositeLib"); - - var testCase = new R2RTestCase + new R2RTestRunner().Run(new R2RTestCase { Name = "CompositeBasic", MainSourceResourceName = "CrossModuleInlining/CompositeBasic.cs", + CompositeMode = true, Dependencies = new List { - new CompiledAssembly + new() { AssemblyName = "CompositeLib", - SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/CompositeLib.cs" }, + SourceResourceNames = ["CrossModuleInlining/Dependencies/CompositeLib.cs"], IsCrossgenInput = true, } }, - Expectations = expectations, - }; - - new R2RTestRunner().Run(testCase); + Validate = reader => + { + R2RAssert.HasManifestRef(reader, "CompositeLib"); + }, + }); } /// @@ -141,22 +130,20 @@ public void CompositeBasic() [Fact] public void RuntimeAsyncMethodEmission() { - var expectations = new R2RExpectations(); - expectations.Features.Add(RuntimeAsyncFeature); - expectations.ExpectedAsyncVariantMethods.Add("SimpleAsyncMethod"); - expectations.ExpectedAsyncVariantMethods.Add("AsyncVoidReturn"); - expectations.ExpectedAsyncVariantMethods.Add("ValueTaskMethod"); - - var testCase = new R2RTestCase + new R2RTestRunner().Run(new R2RTestCase { Name = "RuntimeAsyncMethodEmission", MainSourceResourceName = "RuntimeAsync/BasicAsyncEmission.cs", - MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, + MainExtraSourceResourceNames = ["RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"], + Features = { RuntimeAsyncFeature }, Dependencies = new List(), - Expectations = expectations, - }; - - new R2RTestRunner().Run(testCase); + Validate = reader => + { + R2RAssert.HasAsyncVariant(reader, "SimpleAsyncMethod"); + R2RAssert.HasAsyncVariant(reader, "AsyncVoidReturn"); + R2RAssert.HasAsyncVariant(reader, "ValueTaskMethod"); + }, + }); } /// @@ -167,25 +154,21 @@ public void RuntimeAsyncMethodEmission() [Fact] public void RuntimeAsyncContinuationLayout() { - var expectations = new R2RExpectations - { - ExpectContinuationLayout = true, - ExpectResumptionStubFixup = true, - }; - expectations.Features.Add(RuntimeAsyncFeature); - expectations.ExpectedAsyncVariantMethods.Add("CaptureObjectAcrossAwait"); - expectations.ExpectedAsyncVariantMethods.Add("CaptureMultipleRefsAcrossAwait"); - - var testCase = new R2RTestCase + new R2RTestRunner().Run(new R2RTestCase { Name = "RuntimeAsyncContinuationLayout", MainSourceResourceName = "RuntimeAsync/AsyncWithContinuation.cs", - MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, + MainExtraSourceResourceNames = ["RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"], + Features = { RuntimeAsyncFeature }, Dependencies = new List(), - Expectations = expectations, - }; - - new R2RTestRunner().Run(testCase); + Validate = reader => + { + R2RAssert.HasAsyncVariant(reader, "CaptureObjectAcrossAwait"); + R2RAssert.HasAsyncVariant(reader, "CaptureMultipleRefsAcrossAwait"); + R2RAssert.HasContinuationLayout(reader); + R2RAssert.HasResumptionStubFixup(reader); + }, + }); } /// @@ -195,20 +178,18 @@ public void RuntimeAsyncContinuationLayout() [Fact] public void RuntimeAsyncDevirtualize() { - var expectations = new R2RExpectations(); - expectations.Features.Add(RuntimeAsyncFeature); - expectations.ExpectedAsyncVariantMethods.Add("GetValueAsync"); - - var testCase = new R2RTestCase + new R2RTestRunner().Run(new R2RTestCase { Name = "RuntimeAsyncDevirtualize", MainSourceResourceName = "RuntimeAsync/AsyncDevirtualize.cs", - MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, + MainExtraSourceResourceNames = ["RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"], + Features = { RuntimeAsyncFeature }, Dependencies = new List(), - Expectations = expectations, - }; - - new R2RTestRunner().Run(testCase); + Validate = reader => + { + R2RAssert.HasAsyncVariant(reader, "GetValueAsync"); + }, + }); } /// @@ -218,21 +199,19 @@ public void RuntimeAsyncDevirtualize() [Fact] public void RuntimeAsyncNoYield() { - var expectations = new R2RExpectations(); - expectations.Features.Add(RuntimeAsyncFeature); - expectations.ExpectedAsyncVariantMethods.Add("AsyncButNoAwait"); - expectations.ExpectedAsyncVariantMethods.Add("AsyncWithConditionalAwait"); - - var testCase = new R2RTestCase + new R2RTestRunner().Run(new R2RTestCase { Name = "RuntimeAsyncNoYield", MainSourceResourceName = "RuntimeAsync/AsyncNoYield.cs", - MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, + MainExtraSourceResourceNames = ["RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"], + Features = { RuntimeAsyncFeature }, Dependencies = new List(), - Expectations = expectations, - }; - - new R2RTestRunner().Run(testCase); + Validate = reader => + { + R2RAssert.HasAsyncVariant(reader, "AsyncButNoAwait"); + R2RAssert.HasAsyncVariant(reader, "AsyncWithConditionalAwait"); + }, + }); } /// @@ -242,34 +221,32 @@ public void RuntimeAsyncNoYield() [Fact] public void RuntimeAsyncCrossModule() { - var expectations = new R2RExpectations(); - expectations.Features.Add(RuntimeAsyncFeature); - expectations.ExpectedManifestRefs.Add("AsyncDepLib"); - expectations.ExpectedAsyncVariantMethods.Add("CallCrossModuleAsync"); - expectations.Crossgen2Options.Add(Crossgen2Option.CrossModuleOptimization("AsyncDepLib")); - - var testCase = new R2RTestCase + new R2RTestRunner().Run(new R2RTestCase { Name = "RuntimeAsyncCrossModule", MainSourceResourceName = "RuntimeAsync/AsyncCrossModule.cs", - MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, + MainExtraSourceResourceNames = ["RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"], + Features = { RuntimeAsyncFeature }, + Crossgen2Options = { Crossgen2Option.CrossModuleOptimization("AsyncDepLib") }, Dependencies = new List { - new CompiledAssembly + new() { AssemblyName = "AsyncDepLib", - SourceResourceNames = new[] - { + SourceResourceNames = + [ "RuntimeAsync/Dependencies/AsyncDepLib.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" - }, + ], IsCrossgenInput = true, Features = { RuntimeAsyncFeature }, } }, - Expectations = expectations, - }; - - new R2RTestRunner().Run(testCase); + Validate = reader => + { + R2RAssert.HasManifestRef(reader, "AsyncDepLib"); + R2RAssert.HasAsyncVariant(reader, "CallCrossModuleAsync"); + }, + }); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index 23f7d341591640..29541f49fb1e28 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -14,224 +14,128 @@ namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; /// -/// Parsed expectations from a test case's assembly-level and method-level attributes. +/// Static assertion helpers for validating R2R images via . +/// Use these in callbacks. /// -internal sealed class R2RExpectations +internal static class R2RAssert { - public List ExpectedManifestRefs { get; } = new(); - public List ExpectedInlinedMethods { get; } = new(); - public bool CompositeMode { get; set; } - public List Crossgen2Options { get; } = new(); /// - /// Roslyn feature flags for the main assembly (e.g. runtime-async=on). + /// Returns all methods (assembly methods + instance methods) from the reader. /// - public List> Features { get; } = new(); - /// - /// Method names expected to have [ASYNC] variant entries in the R2R image. - /// - public List ExpectedAsyncVariantMethods { get; } = new(); - /// - /// Method names expected to have [RESUME] (resumption stub) entries. - /// - public List ExpectedResumptionStubs { get; } = new(); - /// - /// If true, expect at least one ContinuationLayout fixup in the image. - /// - public bool ExpectContinuationLayout { get; set; } - /// - /// If true, expect at least one ResumptionStubEntryPoint fixup in the image. - /// - public bool ExpectResumptionStubFixup { get; set; } - /// - /// Fixup kinds that must be present somewhere in the image. - /// - public List ExpectedFixupKinds { get; } = new(); -} - -internal sealed record ExpectedInlinedMethod(string MethodName); - -/// -/// Validates R2R images against test expectations using ReadyToRunReader. -/// -internal sealed class R2RResultChecker -{ - /// - /// Validates the main R2R image against expectations. - /// - public void Check(string r2rImagePath, R2RExpectations expectations) + public static List GetAllMethods(ReadyToRunReader reader) { - Assert.True(File.Exists(r2rImagePath), $"R2R image not found: {r2rImagePath}"); - - using var fileStream = File.OpenRead(r2rImagePath); - using var peReader = new PEReader(fileStream); - - Assert.True(ReadyToRunReader.IsReadyToRunImage(peReader), - $"'{Path.GetFileName(r2rImagePath)}' is not a valid R2R image"); - - var reader = new ReadyToRunReader(new SimpleAssemblyResolver(), r2rImagePath); + var methods = new List(); + foreach (var assembly in reader.ReadyToRunAssemblies) + methods.AddRange(assembly.Methods); + foreach (var instanceMethod in reader.InstanceMethods) + methods.Add(instanceMethod.Method); - CheckManifestRefs(reader, expectations, r2rImagePath); - CheckInlinedMethods(reader, expectations, r2rImagePath); - CheckAsyncVariantMethods(reader, expectations, r2rImagePath); - CheckResumptionStubs(reader, expectations, r2rImagePath); - CheckFixupKinds(reader, expectations, r2rImagePath); + return methods; } - private static void CheckManifestRefs(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + /// + /// Asserts the R2R image contains a manifest or MSIL assembly reference with the given name. + /// + public static void HasManifestRef(ReadyToRunReader reader, string assemblyName) { - if (expectations.ExpectedManifestRefs.Count == 0) - return; - - // Get all assembly references (both MSIL and manifest) var allRefs = new HashSet(StringComparer.OrdinalIgnoreCase); - // Read MSIL AssemblyRef table var globalMetadata = reader.GetGlobalMetadata(); var mdReader = globalMetadata.MetadataReader; foreach (var handle in mdReader.AssemblyReferences) { var assemblyRef = mdReader.GetAssemblyReference(handle); - string name = mdReader.GetString(assemblyRef.Name); - allRefs.Add(name); + allRefs.Add(mdReader.GetString(assemblyRef.Name)); } - // Read manifest references (extra refs beyond MSIL table) foreach (var kvp in reader.ManifestReferenceAssemblies) - { allRefs.Add(kvp.Key); - } - foreach (string expected in expectations.ExpectedManifestRefs) - { - Assert.True(allRefs.Contains(expected), - $"Expected assembly reference '{expected}' not found in R2R image '{Path.GetFileName(imagePath)}'. " + - $"Found: [{string.Join(", ", allRefs.OrderBy(s => s))}]"); - } + Assert.True(allRefs.Contains(assemblyName), + $"Expected assembly reference '{assemblyName}' not found. " + + $"Found: [{string.Join(", ", allRefs.OrderBy(s => s))}]"); } - private static void CheckInlinedMethods(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + /// + /// Asserts the R2R image contains a CHECK_IL_BODY fixup whose signature contains the given method name. + /// + public static void HasInlinedMethod(ReadyToRunReader reader, string methodName) { - if (expectations.ExpectedInlinedMethods.Count == 0) - return; - - var checkIlBodySignatures = new HashSet(StringComparer.OrdinalIgnoreCase); var formattingOptions = new SignatureFormattingOptions(); - var allFixupSummary = new List(); + var checkIlBodySigs = new HashSet(StringComparer.OrdinalIgnoreCase); - void CollectFixups(ReadyToRunMethod method) + foreach (var method in GetAllMethods(reader)) { + if (method.Fixups is null) + continue; foreach (var cell in method.Fixups) { - if (cell.Signature is null) - continue; - - string sigText = cell.Signature.ToString(formattingOptions); - allFixupSummary.Add($"[{cell.Signature.FixupKind}] {sigText}"); - - if (cell.Signature.FixupKind is ReadyToRunFixupKind.Check_IL_Body or ReadyToRunFixupKind.Verify_IL_Body) - { - checkIlBodySignatures.Add(sigText); - } + if (cell.Signature?.FixupKind is ReadyToRunFixupKind.Check_IL_Body or ReadyToRunFixupKind.Verify_IL_Body) + checkIlBodySigs.Add(cell.Signature.ToString(formattingOptions)); } } - foreach (var assembly in reader.ReadyToRunAssemblies) - { - foreach (var method in assembly.Methods) - CollectFixups(method); - } - - foreach (var instanceMethod in reader.InstanceMethods) - CollectFixups(instanceMethod.Method); - - foreach (var expected in expectations.ExpectedInlinedMethods) - { - bool found = checkIlBodySignatures.Any(f => - f.Contains(expected.MethodName, StringComparison.OrdinalIgnoreCase)); - - Assert.True(found, - $"Expected CHECK_IL_BODY fixup for '{expected.MethodName}' not found in '{Path.GetFileName(imagePath)}'. " + - $"CHECK_IL_BODY fixups: [{string.Join(", ", checkIlBodySignatures)}]. " + - $"All fixups: [{string.Join("; ", allFixupSummary)}]"); - } - } - - private static List GetAllMethods(ReadyToRunReader reader) - { - var methods = new List(); - foreach (var assembly in reader.ReadyToRunAssemblies) - methods.AddRange(assembly.Methods); - foreach (var instanceMethod in reader.InstanceMethods) - methods.Add(instanceMethod.Method); - - return methods; + Assert.True( + checkIlBodySigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)), + $"Expected CHECK_IL_BODY fixup for '{methodName}' not found. " + + $"CHECK_IL_BODY fixups: [{string.Join(", ", checkIlBodySigs)}]"); } - private static void CheckAsyncVariantMethods(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + /// + /// Asserts the R2R image contains an [ASYNC] variant entry whose signature contains the given method name. + /// + public static void HasAsyncVariant(ReadyToRunReader reader, string methodName) { - if (expectations.ExpectedAsyncVariantMethods.Count == 0) - return; - - var allMethods = GetAllMethods(reader); - var asyncMethods = allMethods + var asyncSigs = GetAllMethods(reader) .Where(m => m.SignatureString.Contains("[ASYNC]", StringComparison.OrdinalIgnoreCase)) .Select(m => m.SignatureString) .ToList(); - foreach (string expected in expectations.ExpectedAsyncVariantMethods) - { - bool found = asyncMethods.Any(sig => - sig.Contains(expected, StringComparison.OrdinalIgnoreCase)); - - Assert.True(found, - $"Expected [ASYNC] variant for '{expected}' not found in '{Path.GetFileName(imagePath)}'. " + - $"Async methods: [{string.Join(", ", asyncMethods)}]. " + - $"All methods: [{string.Join(", ", allMethods.Select(m => m.SignatureString).Take(30))}]"); - } + Assert.True( + asyncSigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)), + $"Expected [ASYNC] variant for '{methodName}' not found. " + + $"Async methods: [{string.Join(", ", asyncSigs)}]"); } - private static void CheckResumptionStubs(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + /// + /// Asserts the R2R image contains a [RESUME] stub entry whose signature contains the given method name. + /// + public static void HasResumptionStub(ReadyToRunReader reader, string methodName) { - if (expectations.ExpectedResumptionStubs.Count == 0 && !expectations.ExpectResumptionStubFixup) - return; - - var allMethods = GetAllMethods(reader); - var resumeMethods = allMethods + var resumeSigs = GetAllMethods(reader) .Where(m => m.SignatureString.Contains("[RESUME]", StringComparison.OrdinalIgnoreCase)) .Select(m => m.SignatureString) .ToList(); - foreach (string expected in expectations.ExpectedResumptionStubs) - { - bool found = resumeMethods.Any(sig => - sig.Contains(expected, StringComparison.OrdinalIgnoreCase)); - - Assert.True(found, - $"Expected [RESUME] stub for '{expected}' not found in '{Path.GetFileName(imagePath)}'. " + - $"Resume methods: [{string.Join(", ", resumeMethods)}]. " + - $"All methods: [{string.Join(", ", allMethods.Select(m => m.SignatureString).Take(30))}]"); - } - - if (expectations.ExpectResumptionStubFixup) - { - var formattingOptions = new SignatureFormattingOptions(); - bool hasResumptionFixup = allMethods.Any(m => - m.Fixups.Any(c => - c.Signature?.FixupKind == ReadyToRunFixupKind.ResumptionStubEntryPoint)); + Assert.True( + resumeSigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)), + $"Expected [RESUME] stub for '{methodName}' not found. " + + $"Resume methods: [{string.Join(", ", resumeSigs)}]"); + } - Assert.True(hasResumptionFixup, - $"Expected ResumptionStubEntryPoint fixup not found in '{Path.GetFileName(imagePath)}'."); - } + /// + /// Asserts the R2R image contains at least one ContinuationLayout fixup. + /// + public static void HasContinuationLayout(ReadyToRunReader reader) + { + HasFixupKind(reader, ReadyToRunFixupKind.ContinuationLayout); } - private static void CheckFixupKinds(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) + /// + /// Asserts the R2R image contains at least one ResumptionStubEntryPoint fixup. + /// + public static void HasResumptionStubFixup(ReadyToRunReader reader) { - if (expectations.ExpectedFixupKinds.Count == 0 && !expectations.ExpectContinuationLayout) - return; + HasFixupKind(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint); + } - var allMethods = GetAllMethods(reader); + /// + /// Asserts the R2R image contains at least one fixup of the given kind. + /// + public static void HasFixupKind(ReadyToRunReader reader, ReadyToRunFixupKind kind) + { var presentKinds = new HashSet(); - foreach (var method in allMethods) + foreach (var method in GetAllMethods(reader)) { if (method.Fixups is null) continue; @@ -242,19 +146,9 @@ private static void CheckFixupKinds(ReadyToRunReader reader, R2RExpectations exp } } - if (expectations.ExpectContinuationLayout) - { - Assert.True(presentKinds.Contains(ReadyToRunFixupKind.ContinuationLayout), - $"Expected ContinuationLayout fixup not found in '{Path.GetFileName(imagePath)}'. " + - $"Present fixup kinds: [{string.Join(", ", presentKinds)}]"); - } - - foreach (var expectedKind in expectations.ExpectedFixupKinds) - { - Assert.True(presentKinds.Contains(expectedKind), - $"Expected fixup kind '{expectedKind}' not found in '{Path.GetFileName(imagePath)}'. " + - $"Present fixup kinds: [{string.Join(", ", presentKinds)}]"); - } + Assert.True(presentKinds.Contains(kind), + $"Expected fixup kind '{kind}' not found. " + + $"Present kinds: [{string.Join(", ", presentKinds)}]"); } } @@ -263,12 +157,11 @@ private static void CheckFixupKinds(ReadyToRunReader reader, R2RExpectations exp /// internal sealed class SimpleAssemblyResolver : IAssemblyResolver { - private readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase); - public IAssemblyMetadata? FindAssembly(MetadataReader metadataReader, AssemblyReferenceHandle assemblyReferenceHandle, string parentFile) { var assemblyRef = metadataReader.GetAssemblyReference(assemblyReferenceHandle); string name = metadataReader.GetString(assemblyRef.Name); + return FindAssembly(name, parentFile); } @@ -280,10 +173,7 @@ internal sealed class SimpleAssemblyResolver : IAssemblyResolver string candidate = Path.Combine(dir, simpleName + ".dll"); if (!File.Exists(candidate)) - { - // Try in runtime pack candidate = Path.Combine(TestPaths.RuntimePackDir, simpleName + ".dll"); - } if (!File.Exists(candidate)) return null; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs index fcafd2860df3a5..c890a059e6cd73 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs @@ -5,13 +5,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using ILCompiler.ReadyToRun.Tests.Expectations; +using System.Reflection.PortableExecutable; +using ILCompiler.Reflection.ReadyToRun; using Xunit; namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; /// -/// Describes a test case: a main source file with its dependencies and expectations. +/// Describes a test case: a main source file with its dependencies and compilation options. +/// Validation is done via the callback which receives the . /// internal sealed class R2RTestCase { @@ -22,7 +24,20 @@ internal sealed class R2RTestCase /// public string[]? MainExtraSourceResourceNames { get; init; } public required List Dependencies { get; init; } - public required R2RExpectations Expectations { get; init; } + + // Compilation config + public bool CompositeMode { get; init; } + public List Crossgen2Options { get; init; } = new(); + /// + /// Roslyn feature flags for the main assembly (e.g. runtime-async=on). + /// + public List> Features { get; init; } = new(); + + /// + /// Callback that receives the for the main R2R image. + /// Use helpers or raw xUnit assertions to validate the output. + /// + public required Action Validate { get; init; } } /// @@ -95,7 +110,7 @@ public void Run(R2RTestCase testCase) var mainRefs = compiledDeps.Select(d => d.IlPath).ToList(); string mainIlPath = compiler.CompileAssembly(testCase.Name, mainSources, mainRefs, outputKind: Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary, - features: testCase.Expectations.Features.Count > 0 ? testCase.Expectations.Features : null); + features: testCase.Features.Count > 0 ? testCase.Features : null); // Step 3: Crossgen2 dependencies var driver = new R2RDriver(); @@ -121,9 +136,9 @@ public void Run(R2RTestCase testCase) // Step 4: Crossgen2 main assembly string mainR2RPath = Path.Combine(r2rDir, Path.GetFileName(mainIlPath)); - if (testCase.Expectations.CompositeMode) + if (testCase.CompositeMode) { - RunCompositeCompilation(testCase, driver, ilDir, r2rDir, mainIlPath, mainR2RPath, allRefPaths, compiledDeps); + RunCompositeCompilation(testCase, driver, mainIlPath, mainR2RPath, allRefPaths, compiledDeps); } else { @@ -131,8 +146,10 @@ public void Run(R2RTestCase testCase) } // Step 5: Validate R2R output - var checker = new R2RResultChecker(); - checker.Check(mainR2RPath, testCase.Expectations); + Assert.True(File.Exists(mainR2RPath), $"R2R image not found: {mainR2RPath}"); + + var reader = new ReadyToRunReader(new SimpleAssemblyResolver(), mainR2RPath); + testCase.Validate(reader); } finally { @@ -157,7 +174,7 @@ private static void RunSingleCompilation( InputPath = mainIlPath, OutputPath = mainR2RPath, ReferencePaths = allRefPaths, - ExtraArgs = testCase.Expectations.Crossgen2Options.SelectMany(o => o.ToArgs()).ToList(), + ExtraArgs = testCase.Crossgen2Options.SelectMany(o => o.ToArgs()).ToList(), }; var result = driver.Compile(options); @@ -168,8 +185,6 @@ private static void RunSingleCompilation( private static void RunCompositeCompilation( R2RTestCase testCase, R2RDriver driver, - string ilDir, - string r2rDir, string mainIlPath, string mainR2RPath, List allRefPaths, @@ -189,7 +204,7 @@ private static void RunCompositeCompilation( ReferencePaths = allRefPaths, Composite = true, CompositeInputPaths = compositeInputs, - ExtraArgs = testCase.Expectations.Crossgen2Options.SelectMany(o => o.ToArgs()).ToList(), + ExtraArgs = testCase.Crossgen2Options.SelectMany(o => o.ToArgs()).ToList(), }; var result = driver.Compile(options); From 25ee0beef18ef81bc254e51d8bf4c4b961459a9d Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:09:53 -0700 Subject: [PATCH 16/74] Fix R2R test runner: --opt-cross-module args and composite output naming - Fix --opt-cross-module arg emission: use space-separated args without .dll suffix instead of colon-joined format - Add -composite suffix to composite output FilePath to prevent component stubs from overwriting the composite image - Add IsComposite property to CrossgenCompilation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCasesRunner/R2RDriver.cs | 179 ++++----- .../TestCasesRunner/R2RTestCaseCompiler.cs | 6 +- .../TestCasesRunner/R2RTestRunner.cs | 379 +++++++++++------- .../TestCasesRunner/TestPaths.cs | 30 +- 4 files changed, 321 insertions(+), 273 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs index 5eb4d40719d90a..7b4fe7b11fb491 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs @@ -6,31 +6,65 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; +using Xunit.Abstractions; namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; /// /// Known crossgen2 option kinds. /// -internal enum Crossgen2OptionKind +internal enum Crossgen2AssemblyOption { - /// Enables cross-module inlining for a named assembly (--opt-cross-module:AssemblyName). + /// Enables cross-module inlining for a named assembly (--opt-cross-module AssemblyName). CrossModuleOptimization, } -/// -/// A typed crossgen2 option with optional parameter value. -/// -internal sealed record Crossgen2Option(Crossgen2OptionKind Kind, string? Value = null) +internal enum Crossgen2InputKind { - public static Crossgen2Option CrossModuleOptimization(string assemblyName) - => new(Crossgen2OptionKind.CrossModuleOptimization, assemblyName); + InputAssembly, + Reference, + InputBubbleReference, + UnrootedInputFile, +} + +internal enum Crossgen2Option +{ + Composite, + InputBubble, + ObjectFormat, + HotColdSplitting, + Optimize, + TargetArch, + TargetOS, +} + +internal static class Crossgen2OptionsExtensions +{ + public static string ToArg(this Crossgen2AssemblyOption kind) => kind switch + { + Crossgen2AssemblyOption.CrossModuleOptimization => $"--opt-cross-module", + _ => throw new ArgumentOutOfRangeException(nameof(kind)), + }; + + public static string ToArg(this Crossgen2InputKind kind) => kind switch + { + Crossgen2InputKind.InputAssembly => "", // positional argument + Crossgen2InputKind.Reference => $"--reference", + Crossgen2InputKind.InputBubbleReference => $"--inputbubbleref", + Crossgen2InputKind.UnrootedInputFile => $"--unrooted-input-file-paths", + _ => throw new ArgumentOutOfRangeException(nameof(kind)), + }; - public IEnumerable ToArgs() => Kind switch + public static string ToArg(this Crossgen2Option kind) => kind switch { - Crossgen2OptionKind.CrossModuleOptimization => [$"--opt-cross-module:{Value}"], - _ => throw new ArgumentOutOfRangeException(nameof(Kind)), + Crossgen2Option.Composite => $"--composite", + Crossgen2Option.InputBubble => $"--input-bubble", + Crossgen2Option.ObjectFormat => $"--object-format", + Crossgen2Option.HotColdSplitting => $"--hot-cold-splitting", + Crossgen2Option.Optimize => $"--optimize", + Crossgen2Option.TargetArch => $"--target-arch", + Crossgen2Option.TargetOS => $"--target-os", + _ => throw new ArgumentOutOfRangeException(nameof(kind)), }; } @@ -38,7 +72,6 @@ public static Crossgen2Option CrossModuleOptimization(string assemblyName) /// Result of a crossgen2 compilation step. /// internal sealed record R2RCompilationResult( - string OutputPath, int ExitCode, string StandardOutput, string StandardError) @@ -46,114 +79,41 @@ internal sealed record R2RCompilationResult( public bool Success => ExitCode == 0; } -/// -/// Options for a single crossgen2 compilation step. -/// -internal sealed class R2RCompilationOptions -{ - public required string InputPath { get; init; } - public required string OutputPath { get; init; } - public List ReferencePaths { get; init; } = new(); - public List ExtraArgs { get; init; } = new(); - public bool Composite { get; init; } - public List? CompositeInputPaths { get; init; } - public List? InputBubbleRefs { get; init; } -} - /// /// Invokes crossgen2 out-of-process to produce R2R images. /// internal sealed class R2RDriver { - private readonly string _crossgen2Dir; + private static readonly TimeSpan ProcessTimeout = TimeSpan.FromMinutes(2); + private readonly ITestOutputHelper _output; - public R2RDriver() + public R2RDriver(ITestOutputHelper output) { - _crossgen2Dir = TestPaths.Crossgen2Dir; + _output = output; - if (!File.Exists(TestPaths.Crossgen2Dll)) - throw new FileNotFoundException($"crossgen2.dll not found at {TestPaths.Crossgen2Dll}"); + if (!File.Exists(TestPaths.Crossgen2Exe)) + throw new FileNotFoundException($"crossgen2 executable not found at {TestPaths.Crossgen2Exe}"); } /// - /// Runs crossgen2 on a single assembly. + /// Runs crossgen2 with the given arguments. /// - public R2RCompilationResult Compile(R2RCompilationOptions options) + public R2RCompilationResult Compile(List args) { - var args = new List(); - - if (options.Composite) - { - args.Add("--composite"); - if (options.CompositeInputPaths is not null) - { - foreach (string input in options.CompositeInputPaths) - args.Add(input); - } - } - else - { - args.Add(options.InputPath); - } - - args.Add("-o"); - args.Add(options.OutputPath); - - foreach (string refPath in options.ReferencePaths) - { - args.Add("-r"); - args.Add(refPath); - } - - if (options.InputBubbleRefs is not null) - { - foreach (string bubbleRef in options.InputBubbleRefs) - { - args.Add("--inputbubbleref"); - args.Add(bubbleRef); - } - } - - args.AddRange(options.ExtraArgs); - - return RunCrossgen2(args); - } - - /// - /// Crossgen2 a dependency assembly (simple single-assembly R2R). - /// - public R2RCompilationResult CompileDependency(string inputPath, string outputPath, IEnumerable referencePaths) - { - return Compile(new R2RCompilationOptions - { - InputPath = inputPath, - OutputPath = outputPath, - ReferencePaths = referencePaths.ToList() - }); + var fullArgs = new List(args); + return RunCrossgen2(fullArgs); } private R2RCompilationResult RunCrossgen2(List crossgen2Args) { - // Use dotnet exec to invoke crossgen2.dll - string dotnetHost = TestPaths.DotNetHost; - string crossgen2Dll = TestPaths.Crossgen2Dll; - - var allArgs = new List { "exec", crossgen2Dll }; - allArgs.AddRange(crossgen2Args); - - string argsString = string.Join(" ", allArgs.Select(QuoteIfNeeded)); - - var psi = new ProcessStartInfo + var psi = new ProcessStartInfo(TestPaths.Crossgen2Exe, crossgen2Args) { - FileName = dotnetHost, - Arguments = argsString, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; - // Strip environment variables that interfere with crossgen2 string[] envVarsToStrip = { "DOTNET_GCName", "DOTNET_GCStress", "DOTNET_HeapVerify", "DOTNET_ReadyToRun" }; foreach (string envVar in envVarsToStrip) { @@ -163,24 +123,23 @@ private R2RCompilationResult RunCrossgen2(List crossgen2Args) using var process = Process.Start(psi)!; string stdout = process.StandardOutput.ReadToEnd(); string stderr = process.StandardError.ReadToEnd(); - process.WaitForExit(); - string outputPath = crossgen2Args - .SkipWhile(a => a != "-o") - .Skip(1) - .FirstOrDefault() ?? "unknown"; + if (!process.WaitForExit(ProcessTimeout)) + { + try { process.Kill(entireProcessTree: true); } + catch { /* best effort */ } + throw new TimeoutException($"crossgen2 timed out after {ProcessTimeout.TotalMinutes} minutes"); + } + + if (process.ExitCode != 0) + { + _output.WriteLine($" crossgen2 FAILED (exit code {process.ExitCode})"); + _output.WriteLine(stderr); + } return new R2RCompilationResult( - outputPath, process.ExitCode, stdout, stderr); } - - private static string QuoteIfNeeded(string arg) - { - if (arg.Contains(' ') || arg.Contains('"')) - return $"\"{arg.Replace("\"", "\\\"")}\""; - return arg; - } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs index c92c52d16c58af..8293a31a82e513 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs @@ -18,12 +18,10 @@ namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; /// internal sealed class R2RTestCaseCompiler { - private readonly string _outputDir; private readonly List _frameworkReferences; - public R2RTestCaseCompiler(string outputDir) + public R2RTestCaseCompiler() { - _outputDir = outputDir; _frameworkReferences = new List(); // Add reference assemblies from the ref pack (needed for Roslyn compilation) @@ -58,6 +56,7 @@ public R2RTestCaseCompiler(string outputDir) public string CompileAssembly( string assemblyName, IEnumerable sources, + string outputPath, IEnumerable? additionalReferences = null, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, IEnumerable? additionalDefines = null, @@ -91,7 +90,6 @@ public string CompileAssembly( .WithAllowUnsafe(true) .WithNullableContextOptions(NullableContextOptions.Enable)); - string outputPath = Path.Combine(_outputDir, assemblyName + ".dll"); EmitResult result = compilation.Emit(outputPath); if (!result.Success) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs index c890a059e6cd73..867a5158da73a5 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs @@ -5,224 +5,327 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection.PortableExecutable; +using System.Reflection; using ILCompiler.Reflection.ReadyToRun; using Xunit; +using Xunit.Abstractions; namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; /// -/// Describes a test case: a main source file with its dependencies and compilation options. -/// Validation is done via the callback which receives the . +/// Describes an assembly compiled by Roslyn as part of a test case. /// -internal sealed class R2RTestCase +internal sealed class CompiledAssembly { - public required string Name { get; init; } - public required string MainSourceResourceName { get; init; } + public required string AssemblyName { get; init; } + /// - /// Additional source files to compile with the main assembly (e.g. shared attribute files). + /// The name of the string resources that contain the source code for this assembly. /// - public string[]? MainExtraSourceResourceNames { get; init; } - public required List Dependencies { get; init; } + public required string[] SourceResourceNames { get; init; } - // Compilation config - public bool CompositeMode { get; init; } - public List Crossgen2Options { get; init; } = new(); /// - /// Roslyn feature flags for the main assembly (e.g. runtime-async=on). + /// Roslyn feature flags for this assembly (e.g. runtime-async=on). /// public List> Features { get; init; } = new(); /// - /// Callback that receives the for the main R2R image. - /// Use helpers or raw xUnit assertions to validate the output. + /// References to other assemblies that this assembly depends on. /// - public required Action Validate { get; init; } + public List References { get; init; } = new(); + + private string? _outputDir = null; + public string FilePath => _outputDir != null ? Path.Combine(_outputDir, "IL", AssemblyName + ".dll") + : throw new InvalidOperationException("Output directory not set"); + + public void SetOutputDir(string outputDir) + { + _outputDir = outputDir; + } } /// -/// Describes an assembly compiled as part of a test case. -/// Dependencies are compiled in listed order — each assembly can reference all previously compiled assemblies. +/// References a within a , +/// specifying its role and per-assembly options. /// -internal sealed class CompiledAssembly +internal sealed class CrossgenAssembly(CompiledAssembly ilAssembly) { - public required string AssemblyName { get; init; } - public required string[] SourceResourceNames { get; init; } + public CompiledAssembly ILAssembly => ilAssembly; /// - /// If true, this assembly is passed as an input to crossgen2. - /// If false, it is only used as a reference (--ref) during crossgen2 compilation. + /// How this assembly is passed to crossgen2. + /// Defaults to . /// - public bool IsCrossgenInput { get; init; } + public Crossgen2InputKind Kind { get; init; } = Crossgen2InputKind.InputAssembly; /// - /// Roslyn feature flags for this dependency (e.g. runtime-async=on). + /// Per-assembly crossgen2 options (e.g. cross-module optimization targets). /// - public List> Features { get; init; } = new(); + public List Options { get; init; } = new(); + + public void SetOutputDir(string outputDir) + { + ILAssembly.SetOutputDir(outputDir); + } } /// -/// Orchestrates the full R2R test pipeline: compile → crossgen2 → validate. +/// A single crossgen2 compilation step. /// -internal sealed class R2RTestRunner +internal sealed class CrossgenCompilation(string name, List assemblies) { /// - /// Runs a test case end-to-end. + /// Assemblies involved in this compilation. Each specifies its role + /// () and per-assembly options. + /// All other Roslyn-compiled assemblies are automatically available as refs. /// - public void Run(R2RTestCase testCase) - { - string tempDir = Path.Combine(Path.GetTempPath(), "R2RTests", testCase.Name, Guid.NewGuid().ToString("N")[..8]); - string ilDir = Path.Combine(tempDir, "il"); - string r2rDir = Path.Combine(tempDir, "r2r"); + public List Assemblies => assemblies; - try - { - Directory.CreateDirectory(ilDir); - Directory.CreateDirectory(r2rDir); + /// + /// Image-level crossgen2 options (e.g. Composite, InputBubble, Optimize). + /// + public List Options { get; init; } = new(); - // Step 1: Compile all dependencies with Roslyn (in order, leaf to root) - var compiler = new R2RTestCaseCompiler(ilDir); - var compiledDeps = new List<(CompiledAssembly Dep, string IlPath)>(); + /// + /// Optional validator for this compilation's R2R output image. + /// + public Action? Validate { get; init; } - foreach (var dep in testCase.Dependencies) - { - var sources = dep.SourceResourceNames - .Select(R2RTestCaseCompiler.ReadEmbeddedSource) - .ToList(); + public string Name => name; + + public bool IsComposite => Options.Contains(Crossgen2Option.Composite); + + private string? _outputDir = null; - // Each dependency can reference all previously compiled assemblies - var refs = compiledDeps.Select(d => d.IlPath).ToList(); + /// + /// The output path for this compilation. In composite mode, uses a "-composite" suffix + /// to avoid colliding with component stubs that crossgen2 creates alongside the composite image. + /// + public string FilePath => _outputDir != null + ? Path.Combine(_outputDir, "CG2", Name + (IsComposite ? "-composite" : "") + ".dll") + : throw new InvalidOperationException("Output directory not set"); + + public void SetOutputDir(string outputDir) + { + _outputDir = outputDir; + foreach (var assembly in assemblies) + { + assembly.SetOutputDir(outputDir); + } + } +} + +/// +/// Describes a test case: assemblies compiled with Roslyn, then crossgen2'd in one or more +/// compilation steps, each optionally validated. +/// +internal sealed class R2RTestCase(string name, List compilations) +{ + public string Name => name; + + /// + /// One or more crossgen2 compilation steps, executed after Roslyn compilation. + /// Each step can independently produce and validate an R2R image. + /// + public List Compilations => compilations; - string ilPath = compiler.CompileAssembly(dep.AssemblyName, sources, refs, - features: dep.Features.Count > 0 ? dep.Features : null); - compiledDeps.Add((dep, ilPath)); + public IEnumerable GetAssemblies() + { + // Should be a small number of assemblies, so a simple list is fine as an insertion-ordered set + List seen = new(); + foreach (var cg2Compilation in compilations) + { + foreach(var assembly in cg2Compilation.Assemblies) + { + GetDependencies(assembly.ILAssembly, seen); } + } + return seen; - // Step 2: Compile main assembly with Roslyn - var mainSources = new List + IEnumerable GetDependencies(CompiledAssembly assembly, List seen) + { + foreach(var reference in assembly.References) { - R2RTestCaseCompiler.ReadEmbeddedSource(testCase.MainSourceResourceName) - }; - if (testCase.MainExtraSourceResourceNames is not null) + GetDependencies(reference, seen); + } + if (!seen.Contains(assembly)) { - foreach (string extra in testCase.MainExtraSourceResourceNames) - mainSources.Add(R2RTestCaseCompiler.ReadEmbeddedSource(extra)); + seen.Add(assembly); } + return seen; + } + } - var mainRefs = compiledDeps.Select(d => d.IlPath).ToList(); - string mainIlPath = compiler.CompileAssembly(testCase.Name, mainSources, mainRefs, - outputKind: Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary, - features: testCase.Features.Count > 0 ? testCase.Features : null); + public void SetOutputDir(string outputDir) + { + Compilations.ForEach(c => c.SetOutputDir(outputDir)); + } +} - // Step 3: Crossgen2 dependencies - var driver = new R2RDriver(); - var allRefPaths = BuildReferencePaths(ilDir); +/// +/// Orchestrates the full R2R test pipeline: Roslyn compile → crossgen2 → validate. +/// +internal sealed class R2RTestRunner +{ + private readonly ITestOutputHelper _output; - foreach (var (dep, ilPath) in compiledDeps) - { - if (!dep.IsCrossgenInput) - continue; + public R2RTestRunner(ITestOutputHelper output) + { + _output = output; + } - string r2rPath = Path.Combine(r2rDir, Path.GetFileName(ilPath)); - var result = driver.Compile(new R2RCompilationOptions - { - InputPath = ilPath, - OutputPath = r2rPath, - ReferencePaths = allRefPaths, - }); + /// + /// Runs a test case end-to-end. + /// + public void Run(R2RTestCase testCase) + { + var assembliesToCompile = testCase.GetAssemblies(); + Assert.True(assembliesToCompile.Count() > 0, "Test case must have at least one assembly."); + Assert.True(testCase.Compilations.Count > 0, "Test case must have at least one compilation."); - Assert.True(result.Success, - $"crossgen2 failed for dependency '{dep.AssemblyName}':\n{result.StandardError}\n{result.StandardOutput}"); - } + string baseOutputDir = Path.Combine(AppContext.BaseDirectory, "R2RTestCases", testCase.Name, Guid.NewGuid().ToString("N")[..8]); + testCase.SetOutputDir(baseOutputDir); - // Step 4: Crossgen2 main assembly - string mainR2RPath = Path.Combine(r2rDir, Path.GetFileName(mainIlPath)); + _output.WriteLine($"Test '{testCase.Name}': baseOutputDir = {baseOutputDir}"); - if (testCase.CompositeMode) - { - RunCompositeCompilation(testCase, driver, mainIlPath, mainR2RPath, allRefPaths, compiledDeps); - } - else - { - RunSingleCompilation(testCase, driver, mainIlPath, mainR2RPath, allRefPaths); - } + try + { + // Step 1: Compile all assemblies with Roslyn in order + var assemblyPaths = CompileAllAssemblies(assembliesToCompile); + + // Step 2: Run each crossgen2 compilation and validate + var driver = new R2RDriver(_output); + var refPaths = BuildReferencePaths(); - // Step 5: Validate R2R output - Assert.True(File.Exists(mainR2RPath), $"R2R image not found: {mainR2RPath}"); + foreach(var compilation in testCase.Compilations) + { + string outputPath = RunCrossgenCompilation( + testCase.Name, compilation, driver, compilation.FilePath, refPaths, assemblyPaths); - var reader = new ReadyToRunReader(new SimpleAssemblyResolver(), mainR2RPath); - testCase.Validate(reader); + if (compilation.Validate is not null) + { + Assert.True(File.Exists(outputPath), $"R2R image not found: {outputPath}"); + _output.WriteLine($" Validating R2R image: {outputPath}"); + var reader = new ReadyToRunReader(new SimpleAssemblyResolver(), outputPath); + compilation.Validate(reader); + } + } } finally { - // Keep temp directory for debugging if KEEP_R2R_TESTS env var is set if (Environment.GetEnvironmentVariable("KEEP_R2R_TESTS") is null) { - try { Directory.Delete(tempDir, true); } + try { Directory.Delete(baseOutputDir, true); } catch { /* best effort */ } } } } - private static void RunSingleCompilation( - R2RTestCase testCase, - R2RDriver driver, - string mainIlPath, - string mainR2RPath, - List allRefPaths) + private Dictionary CompileAllAssemblies( + IEnumerable assemblies) { - var options = new R2RCompilationOptions + var compiler = new R2RTestCaseCompiler(); + var paths = new Dictionary(); + + foreach (var asm in assemblies) { - InputPath = mainIlPath, - OutputPath = mainR2RPath, - ReferencePaths = allRefPaths, - ExtraArgs = testCase.Crossgen2Options.SelectMany(o => o.ToArgs()).ToList(), - }; + var sources = asm.SourceResourceNames + .Select(R2RTestCaseCompiler.ReadEmbeddedSource) + .ToList(); + + EnsureDirectoryExists(Path.GetDirectoryName(asm.FilePath)); + + string ilPath = compiler.CompileAssembly( + asm.AssemblyName, + sources, + asm.FilePath, + additionalReferences: asm.References.Select(r => r.FilePath).ToList(), + features: asm.Features.Count > 0 ? asm.Features : null); + paths[asm.AssemblyName] = ilPath; + _output.WriteLine($" Roslyn compiled '{asm.AssemblyName}' -> {ilPath}"); + } - var result = driver.Compile(options); - Assert.True(result.Success, - $"crossgen2 failed for main assembly '{testCase.Name}':\n{result.StandardError}\n{result.StandardOutput}"); + return paths; } - private static void RunCompositeCompilation( - R2RTestCase testCase, - R2RDriver driver, - string mainIlPath, - string mainR2RPath, - List allRefPaths, - List<(CompiledAssembly Dep, string IlPath)> compiledDeps) + private static void EnsureDirectoryExists(string? v) { - var compositeInputs = new List { mainIlPath }; - foreach (var (dep, ilPath) in compiledDeps) + if (v is not null && !Directory.Exists(v)) { - if (dep.IsCrossgenInput) - compositeInputs.Add(ilPath); + Directory.CreateDirectory(v); } + } - var options = new R2RCompilationOptions + private static string RunCrossgenCompilation( + string testName, + CrossgenCompilation compilation, + R2RDriver driver, + string outputFile, + List refPaths, + Dictionary assemblyPaths) + { + var args = new List(); + + var inputFiles = new List(); + // Per-assembly inputs and options + foreach (var asm in compilation.Assemblies) { - InputPath = mainIlPath, - OutputPath = mainR2RPath, - ReferencePaths = allRefPaths, - Composite = true, - CompositeInputPaths = compositeInputs, - ExtraArgs = testCase.Crossgen2Options.SelectMany(o => o.ToArgs()).ToList(), - }; - - var result = driver.Compile(options); + var ilAssemblyName = asm.ILAssembly.AssemblyName; + Assert.True(assemblyPaths.ContainsKey(ilAssemblyName), + $"Assembly '{ilAssemblyName}' not found in compiled assemblies."); + + string ilPath = asm.ILAssembly.FilePath; + + if (asm.Kind == Crossgen2InputKind.InputAssembly) + { + inputFiles.Add(ilPath); + } + else + { + args.Add(asm.Kind.ToArg()); + args.Add(ilPath); + } + + foreach (var option in asm.Options) + { + args.Add(option.ToArg()); + args.Add(ilAssemblyName); + } + } + + // Image-level options + foreach (var option in compilation.Options) + args.Add(option.ToArg()); + + // Global refs (all compiled IL assemblies + runtime pack) + AddRefArgs(args, refPaths); + + EnsureDirectoryExists(Path.GetDirectoryName(outputFile)); + + inputFiles.AddRange(args); + args = inputFiles; + args.Add($"--out"); + args.Add($"{outputFile}"); + var result = driver.Compile(args); Assert.True(result.Success, - $"crossgen2 composite compilation failed for '{testCase.Name}':\n{result.StandardError}\n{result.StandardOutput}"); + $"crossgen2 failed for '{testName}':\n{result.StandardError}\n{result.StandardOutput}"); + + return outputFile; } - private static List BuildReferencePaths(string ilDir) + private static void AddRefArgs(List args, List refPaths) { - var paths = new List(); + foreach (string refPath in refPaths) + { + args.Add("-r"); + args.Add(refPath); + } + } - // Add all compiled IL assemblies as references - paths.Add(Path.Combine(ilDir, "*.dll")); + private static List BuildReferencePaths() + { + var paths = new List(); - // Add framework references (managed assemblies) paths.Add(Path.Combine(TestPaths.RuntimePackDir, "*.dll")); - // System.Private.CoreLib is in the native directory, not lib string runtimePackDir = TestPaths.RuntimePackDir; string nativeDir = Path.GetFullPath(Path.Combine(runtimePackDir, "..", "..", "native")); if (Directory.Exists(nativeDir)) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs index 05bc828f18d98e..ecdd840e6b0143 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs @@ -48,9 +48,16 @@ public static string Crossgen2Dir } /// - /// Path to the crossgen2.dll managed assembly. + /// Path to the native crossgen2 executable (apphost). /// - public static string Crossgen2Dll => Path.Combine(Crossgen2Dir, "crossgen2.dll"); + public static string Crossgen2Exe + { + get + { + string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "crossgen2.exe" : "crossgen2"; + return Path.Combine(Crossgen2Dir, exe); + } + } /// /// Path to the runtime pack managed assemblies directory. @@ -130,25 +137,6 @@ public static string RefPackDir } } - /// - /// Returns the dotnet host executable path suitable for running crossgen2. - /// - public static string DotNetHost - { - get - { - string repoRoot = Path.GetFullPath(Path.Combine(CoreCLRArtifactsDir, "..", "..", "..", "..")); - string dotnetDir = Path.Combine(repoRoot, ".dotnet"); - string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; - string path = Path.Combine(dotnetDir, exe); - if (File.Exists(path)) - return path; - - // Fallback to PATH - return exe; - } - } - /// /// Returns the target triple string for crossgen2 (e.g. "linux-x64"). /// From b2e0f5b0cb5e0ebeceb6c86577fa48c932ac87d7 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:10:15 -0700 Subject: [PATCH 17/74] Add InliningInfo2 structured parsing and method-targeted assertions InliningInfoSection2: - Add InliningEntry struct, GetEntries(), GetInliningPairs() - Method name resolution via local method map and OpenReferenceAssembly R2RResultChecker: - HasInlinedMethod checks both CrossModuleInlineInfo and all per-assembly InliningInfo2 sections (needed for composite images) - Add HasContinuationLayout(reader, methodName) overload - Add HasResumptionStubFixup(reader, methodName) overload - Add HasFixupKindOnMethod generic helper - GetAllInliningInfo2Sections iterates global + all assembly headers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCasesRunner/R2RResultChecker.cs | 200 ++++++++++++++++-- .../InliningInfoSection2.cs | 194 ++++++++++++++++- src/coreclr/tools/r2rdump/TextDumper.cs | 6 + 3 files changed, 381 insertions(+), 19 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index 29541f49fb1e28..2cbd2ecb938b97 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -9,6 +9,7 @@ using System.Reflection.PortableExecutable; using ILCompiler.Reflection.ReadyToRun; using Internal.ReadyToRunConstants; +using Internal.Runtime; using Xunit; namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; @@ -41,11 +42,14 @@ public static void HasManifestRef(ReadyToRunReader reader, string assemblyName) var allRefs = new HashSet(StringComparer.OrdinalIgnoreCase); var globalMetadata = reader.GetGlobalMetadata(); - var mdReader = globalMetadata.MetadataReader; - foreach (var handle in mdReader.AssemblyReferences) + if (globalMetadata is not null) { - var assemblyRef = mdReader.GetAssemblyReference(handle); - allRefs.Add(mdReader.GetString(assemblyRef.Name)); + var mdReader = globalMetadata.MetadataReader; + foreach (var handle in mdReader.AssemblyReferences) + { + var assemblyRef = mdReader.GetAssemblyReference(handle); + allRefs.Add(mdReader.GetString(assemblyRef.Name)); + } } foreach (var kvp in reader.ManifestReferenceAssemblies) @@ -57,28 +61,137 @@ public static void HasManifestRef(ReadyToRunReader reader, string assemblyName) } /// - /// Asserts the R2R image contains a CHECK_IL_BODY fixup whose signature contains the given method name. + /// Asserts that the CrossModuleInlineInfo section records that + /// inlined , and that the inlinee is encoded as a cross-module + /// reference (ILBody import index, not a local MethodDef RID). /// - public static void HasInlinedMethod(ReadyToRunReader reader, string methodName) + public static void HasCrossModuleInlinedMethod(ReadyToRunReader reader, string inlinerMethodName, string inlineeMethodName) { - var formattingOptions = new SignatureFormattingOptions(); - var checkIlBodySigs = new HashSet(StringComparer.OrdinalIgnoreCase); + var inliningInfo = GetCrossModuleInliningInfoSection(reader); - foreach (var method in GetAllMethods(reader)) + var allPairs = new List(); + foreach (var (inlinerName, inlineeName, inlineeKind) in inliningInfo.GetInliningPairs()) { - if (method.Fixups is null) - continue; - foreach (var cell in method.Fixups) + allPairs.Add($"{inlinerName} → {inlineeName} ({inlineeKind})"); + + if (inlinerName.Contains(inlinerMethodName, StringComparison.OrdinalIgnoreCase) && + inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) + { + Assert.True(inlineeKind == CrossModuleInliningInfoSection.InlineeReferenceKind.CrossModule, + $"Found inlining pair '{inlinerName} → {inlineeName}' but the inlinee is not encoded " + + $"as a cross-module reference ({inlineeKind}). Expected ILBody import encoding."); + return; + } + } + + Assert.Fail( + $"Expected cross-module inlining '{inlineeMethodName}' into '{inlinerMethodName}', but it was not found.\n" + + $"Recorded inlining pairs:\n {string.Join("\n ", allPairs)}"); + } + + /// + /// Asserts that any inlining info section (CrossModuleInlineInfo or InliningInfo2) records that + /// inlined . + /// Does not check whether the encoding is cross-module or local. + /// + public static void HasInlinedMethod(ReadyToRunReader reader, string inlinerMethodName, string inlineeMethodName) + { + var foundPairs = new List(); + + if (reader.ReadyToRunHeader.Sections.ContainsKey(ReadyToRunSectionType.CrossModuleInlineInfo)) + { + var inliningInfo = GetCrossModuleInliningInfoSection(reader); + foreach (var (inlinerName, inlineeName, _) in inliningInfo.GetInliningPairs()) + { + if (inlinerName.Contains(inlinerMethodName, StringComparison.OrdinalIgnoreCase) && + inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + foundPairs.Add($"[CXMI] {inlinerName} -> {inlineeName}"); + } + } + + foreach (var info2 in GetAllInliningInfo2Sections(reader)) + { + foreach (var (inlinerName, inlineeName) in info2.GetInliningPairs()) + { + if (inlinerName.Contains(inlinerMethodName, StringComparison.OrdinalIgnoreCase) && + inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + foundPairs.Add($"[II2] {inlinerName} -> {inlineeName}"); + } + } + + string pairList = foundPairs.Count > 0 + ? string.Join("\n ", foundPairs) + : "(none)"; + + Assert.Fail( + $"Expected inlining '{inlineeMethodName}' into '{inlinerMethodName}', but it was not found.\n" + + $"Found inlining pairs:\n {pairList}"); + } + + private static CrossModuleInliningInfoSection GetCrossModuleInliningInfoSection(ReadyToRunReader reader) + { + Assert.True( + reader.ReadyToRunHeader.Sections.TryGetValue( + ReadyToRunSectionType.CrossModuleInlineInfo, out ReadyToRunSection section), + "Expected CrossModuleInlineInfo section not found in R2R image."); + + int offset = reader.GetOffset(section.RelativeVirtualAddress); + int endOffset = offset + section.Size; + + return new CrossModuleInliningInfoSection(reader, offset, endOffset); + } + + private static IEnumerable GetAllInliningInfo2Sections(ReadyToRunReader reader) + { + // InliningInfo2 can appear in the global header + if (reader.ReadyToRunHeader.Sections.TryGetValue( + ReadyToRunSectionType.InliningInfo2, out ReadyToRunSection globalSection)) + { + int offset = reader.GetOffset(globalSection.RelativeVirtualAddress); + int endOffset = offset + globalSection.Size; + yield return new InliningInfoSection2(reader, offset, endOffset); + } + + // In composite images, InliningInfo2 is per-assembly + if (reader.ReadyToRunAssemblyHeaders is not null) + { + foreach (var asmHeader in reader.ReadyToRunAssemblyHeaders) { - if (cell.Signature?.FixupKind is ReadyToRunFixupKind.Check_IL_Body or ReadyToRunFixupKind.Verify_IL_Body) - checkIlBodySigs.Add(cell.Signature.ToString(formattingOptions)); + if (asmHeader.Sections.TryGetValue( + ReadyToRunSectionType.InliningInfo2, out ReadyToRunSection asmSection)) + { + int offset = reader.GetOffset(asmSection.RelativeVirtualAddress); + int endOffset = offset + asmSection.Size; + yield return new InliningInfoSection2(reader, offset, endOffset); + } } } + } + + /// + /// Asserts the R2R image contains a CrossModuleInlineInfo section with at least one entry. + /// + public static void HasCrossModuleInliningInfo(ReadyToRunReader reader) + { + Assert.True( + reader.ReadyToRunHeader.Sections.TryGetValue( + ReadyToRunSectionType.CrossModuleInlineInfo, out ReadyToRunSection section), + "Expected CrossModuleInlineInfo section not found in R2R image."); + + int offset = reader.GetOffset(section.RelativeVirtualAddress); + int endOffset = offset + section.Size; + var inliningInfo = new CrossModuleInliningInfoSection(reader, offset, endOffset); + string dump = inliningInfo.ToString(); Assert.True( - checkIlBodySigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)), - $"Expected CHECK_IL_BODY fixup for '{methodName}' not found. " + - $"CHECK_IL_BODY fixups: [{string.Join(", ", checkIlBodySigs)}]"); + dump.Length > 0, + "CrossModuleInlineInfo section is present but contains no entries."); } /// @@ -121,6 +234,15 @@ public static void HasContinuationLayout(ReadyToRunReader reader) HasFixupKind(reader, ReadyToRunFixupKind.ContinuationLayout); } + /// + /// Asserts a method whose signature contains + /// has at least one ContinuationLayout fixup. + /// + public static void HasContinuationLayout(ReadyToRunReader reader, string methodName) + { + HasFixupKindOnMethod(reader, ReadyToRunFixupKind.ContinuationLayout, methodName); + } + /// /// Asserts the R2R image contains at least one ResumptionStubEntryPoint fixup. /// @@ -129,6 +251,15 @@ public static void HasResumptionStubFixup(ReadyToRunReader reader) HasFixupKind(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint); } + /// + /// Asserts a method whose signature contains + /// has at least one ResumptionStubEntryPoint fixup. + /// + public static void HasResumptionStubFixup(ReadyToRunReader reader, string methodName) + { + HasFixupKindOnMethod(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint, methodName); + } + /// /// Asserts the R2R image contains at least one fixup of the given kind. /// @@ -150,6 +281,41 @@ public static void HasFixupKind(ReadyToRunReader reader, ReadyToRunFixupKind kin $"Expected fixup kind '{kind}' not found. " + $"Present kinds: [{string.Join(", ", presentKinds)}]"); } + + /// + /// Asserts a method whose signature contains + /// has at least one fixup of the given kind. + /// + public static void HasFixupKindOnMethod(ReadyToRunReader reader, ReadyToRunFixupKind kind, string methodName) + { + var methodsWithFixup = new List(); + foreach (var method in GetAllMethods(reader)) + { + if (method.Fixups is null) + continue; + + bool hasKind = false; + foreach (var cell in method.Fixups) + { + if (cell.Signature is not null && cell.Signature.FixupKind == kind) + { + hasKind = true; + break; + } + } + + if (hasKind) + { + methodsWithFixup.Add(method.SignatureString); + if (method.SignatureString.Contains(methodName, StringComparison.OrdinalIgnoreCase)) + return; + } + } + + Assert.Fail( + $"Expected fixup kind '{kind}' on method matching '{methodName}', but not found.\n" + + $"Methods with '{kind}' fixups: [{string.Join(", ", methodsWithFixup)}]"); + } } /// diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/InliningInfoSection2.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/InliningInfoSection2.cs index 036c771e1bb215..cf48b4f077f3cb 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/InliningInfoSection2.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/InliningInfoSection2.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Text; @@ -19,6 +22,189 @@ public InliningInfoSection2(ReadyToRunReader reader, int offset, int endOffset) _endOffset = endOffset; } + /// + /// A raw inlining entry: one inlinee with its list of inliners. + /// RIDs are MethodDef row numbers. Module index identifies which component + /// assembly in a composite image the method belongs to (0 = owner module). + /// + public readonly struct InliningEntry + { + public int InlineeRid { get; } + public uint InlineeModuleIndex { get; } + public bool InlineeHasModule { get; } + public IReadOnlyList<(int Rid, uint ModuleIndex, bool HasModule)> Inliners { get; } + + public InliningEntry(int inlineeRid, uint inlineeModuleIndex, bool inlineeHasModule, + IReadOnlyList<(int, uint, bool)> inliners) + { + InlineeRid = inlineeRid; + InlineeModuleIndex = inlineeModuleIndex; + InlineeHasModule = inlineeHasModule; + Inliners = inliners; + } + } + + /// + /// Parses all entries from the InliningInfo2 section. + /// + public List GetEntries() + { + var entries = new List(); + + NativeParser parser = new NativeParser(_r2r.ImageReader, (uint)_startOffset); + NativeHashtable hashtable = new NativeHashtable(_r2r.ImageReader, parser, (uint)_endOffset); + + var enumerator = hashtable.EnumerateAllEntries(); + + NativeParser curParser = enumerator.GetNext(); + while (!curParser.IsNull()) + { + int count = (int)curParser.GetUnsigned(); + int inlineeRidAndFlag = (int)curParser.GetUnsigned(); + count--; + int inlineeRid = inlineeRidAndFlag >> 1; + + uint inlineeModule = 0; + bool inlineeHasModule = (inlineeRidAndFlag & 1) != 0; + if (inlineeHasModule) + { + inlineeModule = curParser.GetUnsigned(); + count--; + } + + var inliners = new List<(int, uint, bool)>(); + int currentRid = 0; + while (count > 0) + { + int inlinerDeltaAndFlag = (int)curParser.GetUnsigned(); + count--; + int inlinerDelta = inlinerDeltaAndFlag >> 1; + currentRid += inlinerDelta; + + uint inlinerModule = 0; + bool inlinerHasModule = (inlinerDeltaAndFlag & 1) != 0; + if (inlinerHasModule) + { + inlinerModule = curParser.GetUnsigned(); + count--; + } + + inliners.Add((currentRid, inlinerModule, inlinerHasModule)); + } + + entries.Add(new InliningEntry(inlineeRid, inlineeModule, inlineeHasModule, inliners)); + curParser = enumerator.GetNext(); + } + + return entries; + } + + /// + /// Returns all inlining pairs with resolved method names. + /// + public IEnumerable<(string InlinerName, string InlineeName)> GetInliningPairs() + { + _localMethodMap ??= BuildLocalMethodMap(); + + foreach (var entry in GetEntries()) + { + string inlineeName = ResolveMethod(entry.InlineeRid, entry.InlineeModuleIndex, entry.InlineeHasModule); + foreach (var (rid, moduleIndex, hasModule) in entry.Inliners) + { + string inlinerName = ResolveMethod(rid, moduleIndex, hasModule); + yield return (inlinerName, inlineeName); + } + } + } + + private string ResolveMethod(int rid, uint moduleIndex, bool hasModule) + { + if (hasModule) + { + string moduleName = TryGetModuleName(moduleIndex); + return $"{moduleName}!{ResolveMethodInModule(rid, moduleIndex)}"; + } + + if (_localMethodMap.TryGetValue((uint)rid, out string name)) + return name; + + return $""; + } + + private string ResolveMethodInModule(int rid, uint moduleIndex) + { + try + { + IAssemblyMetadata asmMeta = _r2r.OpenReferenceAssembly((int)moduleIndex); + if (asmMeta is not null) + { + var mdReader = asmMeta.MetadataReader; + var handle = MetadataTokens.MethodDefinitionHandle(rid); + if (mdReader.GetTableRowCount(TableIndex.MethodDef) >= rid) + { + var methodDef = mdReader.GetMethodDefinition(handle); + string typeName = ""; + if (!methodDef.GetDeclaringType().IsNil) + { + var typeDef = mdReader.GetTypeDefinition(methodDef.GetDeclaringType()); + typeName = mdReader.GetString(typeDef.Name) + "."; + } + return typeName + mdReader.GetString(methodDef.Name); + } + } + } + catch + { + // Fall through to token-based name + } + + return $""; + } + + private string TryGetModuleName(uint moduleIndex) + { + if (moduleIndex == 0 && !_r2r.Composite) + return Path.GetFileNameWithoutExtension(_r2r.Filename); + + try + { + return _r2r.GetReferenceAssemblyName((int)moduleIndex); + } + catch + { + return $""; + } + } + + private Dictionary BuildLocalMethodMap() + { + var map = new Dictionary(); + foreach (var assembly in _r2r.ReadyToRunAssemblies) + { + foreach (var method in assembly.Methods) + { + if (method.MethodHandle.Kind == HandleKind.MethodDefinition) + { + uint methodRid = (uint)MetadataTokens.GetRowNumber((MethodDefinitionHandle)method.MethodHandle); + map[methodRid] = method.SignatureString; + } + } + } + + foreach (var instanceEntry in _r2r.InstanceMethods) + { + if (instanceEntry.Method.MethodHandle.Kind == HandleKind.MethodDefinition) + { + uint methodRid = (uint)MetadataTokens.GetRowNumber((MethodDefinitionHandle)instanceEntry.Method.MethodHandle); + map.TryAdd(methodRid, instanceEntry.Method.SignatureString); + } + } + + return map; + } + + private Dictionary _localMethodMap; + public override string ToString() { StringBuilder sb = new StringBuilder(); @@ -39,7 +225,9 @@ public override string ToString() { uint module = curParser.GetUnsigned(); count--; - string moduleName = _r2r.GetReferenceAssemblyName((int)module); + string moduleName = (int)module < _r2r.ManifestReferences.Count + 1 + ? _r2r.GetReferenceAssemblyName((int)module) + : $""; sb.AppendLine($"Inliners for inlinee {inlineeToken:X8} (module {moduleName}):"); } else @@ -60,7 +248,9 @@ public override string ToString() { uint module = curParser.GetUnsigned(); count--; - string moduleName = _r2r.GetReferenceAssemblyName((int)module); + string moduleName = (int)module < _r2r.ManifestReferences.Count + 1 + ? _r2r.GetReferenceAssemblyName((int)module) + : $""; sb.AppendLine($" {inlinerToken:X8} (module {moduleName})"); } else diff --git a/src/coreclr/tools/r2rdump/TextDumper.cs b/src/coreclr/tools/r2rdump/TextDumper.cs index 60cd0fb09ba35d..370f80c80b917c 100644 --- a/src/coreclr/tools/r2rdump/TextDumper.cs +++ b/src/coreclr/tools/r2rdump/TextDumper.cs @@ -504,6 +504,12 @@ public override void DumpSectionContents(ReadyToRunSection section) InliningInfoSection2 inliningInfoSection2 = new InliningInfoSection2(_r2r, ii2Offset, ii2EndOffset); _writer.WriteLine(inliningInfoSection2.ToString()); break; + case ReadyToRunSectionType.CrossModuleInlineInfo: + int cmiOffset = _r2r.GetOffset(section.RelativeVirtualAddress); + int cmiEndOffset = cmiOffset + section.Size; + CrossModuleInliningInfoSection crossModuleInliningInfo = new CrossModuleInliningInfoSection(_r2r, cmiOffset, cmiEndOffset); + _writer.WriteLine(crossModuleInliningInfo.ToString()); + break; case ReadyToRunSectionType.OwnerCompositeExecutable: int oceOffset = _r2r.GetOffset(section.RelativeVirtualAddress); if (_r2r.Image[oceOffset + section.Size - 1] != 0) From c8c6a35a73806a5bdd1b87168d2e555042eacd87 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:10:34 -0700 Subject: [PATCH 18/74] Add 10 new R2R cross-module/async/composite test cases New tests covering the intersection of cross-module inlining, runtime-async, and composite mode: - CompositeCrossModuleInlining, CompositeTransitive - CompositeAsync, CompositeAsyncCrossModuleInlining, CompositeAsyncTransitive (skipped: async not in composite yet) - AsyncCrossModuleContinuation, AsyncCrossModuleTransitive - MultiStepCompositeConsumer, CompositeAsyncDevirt, RuntimeAsyncNoYield Fix existing tests: BasicCrossModuleInlining (InlineableLib as Reference), TransitiveReferences (CrossModuleOptimization on lib). Use method-targeted HasContinuationLayout/HasResumptionStubFixup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 957 +++++++++++++++--- 1 file changed, 799 insertions(+), 158 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 7fdd8d422af7a9..9ce6b95f4513b7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -1,9 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable xUnit1004 // Test methods should not be skipped — composite+async tests are intentionally deferred + using System.Collections.Generic; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; +using ILCompiler.Reflection.ReadyToRun; using Xunit; +using Xunit.Abstractions; namespace ILCompiler.ReadyToRun.Tests.TestCases; @@ -14,136 +18,190 @@ namespace ILCompiler.ReadyToRun.Tests.TestCases; public class R2RTestSuites { private static readonly KeyValuePair RuntimeAsyncFeature = new("runtime-async", "on"); + private readonly ITestOutputHelper _output; + + public R2RTestSuites(ITestOutputHelper output) + { + _output = output; + } [Fact] public void BasicCrossModuleInlining() { - new R2RTestRunner().Run(new R2RTestCase - { - Name = "BasicCrossModuleInlining", - MainSourceResourceName = "CrossModuleInlining/BasicInlining.cs", - Crossgen2Options = { Crossgen2Option.CrossModuleOptimization("InlineableLib") }, - Dependencies = new List - { - new() - { - AssemblyName = "InlineableLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], - IsCrossgenInput = true, - } - }, - Validate = reader => - { - R2RAssert.HasManifestRef(reader, "InlineableLib"); - R2RAssert.HasInlinedMethod(reader, "GetValue"); - R2RAssert.HasInlinedMethod(reader, "GetString"); - }, - }); + var InlineableLib = new CompiledAssembly + { + AssemblyName = "InlineableLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], + }; + var basicCrossModuleInlining = new CompiledAssembly + { + AssemblyName = "BasicCrossModuleInlining", + SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], + References = [InlineableLib] + }; + + var cgInlineableLib = new CrossgenAssembly(InlineableLib){ Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization] }; + var cgBasicCrossModuleInlining = new CrossgenAssembly(basicCrossModuleInlining); + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(BasicCrossModuleInlining), + [new CrossgenCompilation(basicCrossModuleInlining.AssemblyName, [cgInlineableLib, cgBasicCrossModuleInlining]) { Validate = Validate }]) + ); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "InlineableLib"); + R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetValue", "GetValue"); + R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetString", "GetString"); + R2RAssert.HasCrossModuleInliningInfo(reader); + } } [Fact] public void TransitiveReferences() { - new R2RTestRunner().Run(new R2RTestCase + var externalLib = new CompiledAssembly() { - Name = "TransitiveReferences", - MainSourceResourceName = "CrossModuleInlining/TransitiveReferences.cs", - Crossgen2Options = { Crossgen2Option.CrossModuleOptimization("InlineableLibTransitive") }, - Dependencies = new List - { - new() + AssemblyName = "ExternalLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/ExternalLib.cs"], + }; + var inlineableLibTransitive = new CompiledAssembly() + { + AssemblyName = "InlineableLibTransitive", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLibTransitive.cs"], + References = [externalLib] + }; + var transitiveReferences = new CompiledAssembly() + { + AssemblyName = "TransitiveReferences", + SourceResourceNames = ["CrossModuleInlining/TransitiveReferences.cs"], + References = [inlineableLibTransitive, externalLib] + }; + new R2RTestRunner(_output).Run(new R2RTestCase(nameof(TransitiveReferences), + [ + new("TransitiveReferences", [ + new CrossgenAssembly(transitiveReferences), + new CrossgenAssembly(externalLib) { Kind = Crossgen2InputKind.Reference }, + new CrossgenAssembly(inlineableLibTransitive) + { + Kind = Crossgen2InputKind.Reference, + Options = [Crossgen2AssemblyOption.CrossModuleOptimization], + }, + ]) { - AssemblyName = "ExternalLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/ExternalLib.cs"], - IsCrossgenInput = false, + Validate = reader => + { + R2RAssert.HasManifestRef(reader, "InlineableLibTransitive"); + R2RAssert.HasManifestRef(reader, "ExternalLib"); + R2RAssert.HasCrossModuleInlinedMethod(reader, "TestTransitiveValue", "GetExternalValue"); + }, }, - new() - { - AssemblyName = "InlineableLibTransitive", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLibTransitive.cs"], - IsCrossgenInput = true, - } - }, - Validate = reader => - { - R2RAssert.HasManifestRef(reader, "InlineableLibTransitive"); - R2RAssert.HasManifestRef(reader, "ExternalLib"); - R2RAssert.HasInlinedMethod(reader, "GetExternalValue"); - }, - }); + ])); } [Fact] public void AsyncCrossModuleInlining() { - new R2RTestRunner().Run(new R2RTestCase + var asyncInlineableLib = new CompiledAssembly { - Name = "AsyncCrossModuleInlining", - MainSourceResourceName = "CrossModuleInlining/AsyncMethods.cs", - Crossgen2Options = { Crossgen2Option.CrossModuleOptimization("AsyncInlineableLib") }, - Dependencies = new List - { - new() + AssemblyName = "AsyncInlineableLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncInlineableLib.cs"], + }; + var asyncCrossModuleInlining = new CompiledAssembly + { + AssemblyName = nameof(AsyncCrossModuleInlining), + SourceResourceNames = ["CrossModuleInlining/AsyncMethods.cs"], + References = [asyncInlineableLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(AsyncCrossModuleInlining), + [ + new(nameof(AsyncCrossModuleInlining), + [ + new CrossgenAssembly(asyncCrossModuleInlining), + new CrossgenAssembly(asyncInlineableLib) + { + Kind = Crossgen2InputKind.Reference, + Options = [Crossgen2AssemblyOption.CrossModuleOptimization], + }, + ]) { - AssemblyName = "AsyncInlineableLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncInlineableLib.cs"], - IsCrossgenInput = true, - } - }, - Validate = reader => - { - R2RAssert.HasManifestRef(reader, "AsyncInlineableLib"); - R2RAssert.HasInlinedMethod(reader, "GetValueAsync"); - }, - }); + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "AsyncInlineableLib"); + R2RAssert.HasCrossModuleInlinedMethod(reader, "TestAsyncInline", "GetValueAsync"); + } } [Fact] public void CompositeBasic() { - new R2RTestRunner().Run(new R2RTestCase + var compositeLib = new CompiledAssembly + { + AssemblyName = "CompositeLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/CompositeLib.cs"], + }; + var compositeBasic = new CompiledAssembly { - Name = "CompositeBasic", - MainSourceResourceName = "CrossModuleInlining/CompositeBasic.cs", - CompositeMode = true, - Dependencies = new List - { - new() + AssemblyName = nameof(CompositeBasic), + SourceResourceNames = ["CrossModuleInlining/CompositeBasic.cs"], + References = [compositeLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeBasic), + [ + new(nameof(CompositeBasic), + [ + new CrossgenAssembly(compositeLib), + new CrossgenAssembly(compositeBasic), + ]) { - AssemblyName = "CompositeLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/CompositeLib.cs"], - IsCrossgenInput = true, - } - }, - Validate = reader => - { - R2RAssert.HasManifestRef(reader, "CompositeLib"); - }, - }); + Options = [Crossgen2Option.Composite], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "CompositeLib"); + } } - /// - /// PR #124203: Async methods produce [ASYNC] variant entries with resumption stubs. - /// PR #121456: Resumption stubs are emitted as ResumptionStubEntryPoint fixups. - /// PR #123643: Methods with GC refs across awaits produce ContinuationLayout fixups. - /// [Fact] public void RuntimeAsyncMethodEmission() { - new R2RTestRunner().Run(new R2RTestCase + var runtimeAsyncMethodEmission = new CompiledAssembly { - Name = "RuntimeAsyncMethodEmission", - MainSourceResourceName = "RuntimeAsync/BasicAsyncEmission.cs", - MainExtraSourceResourceNames = ["RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"], + AssemblyName = nameof(RuntimeAsyncMethodEmission), + SourceResourceNames = + [ + "RuntimeAsync/BasicAsyncEmission.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], Features = { RuntimeAsyncFeature }, - Dependencies = new List(), - Validate = reader => - { - R2RAssert.HasAsyncVariant(reader, "SimpleAsyncMethod"); - R2RAssert.HasAsyncVariant(reader, "AsyncVoidReturn"); - R2RAssert.HasAsyncVariant(reader, "ValueTaskMethod"); - }, - }); + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(RuntimeAsyncMethodEmission), + [ + new(nameof(RuntimeAsyncMethodEmission), [new CrossgenAssembly(runtimeAsyncMethodEmission)]) + { + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasAsyncVariant(reader, "SimpleAsyncMethod"); + R2RAssert.HasAsyncVariant(reader, "AsyncVoidReturn"); + R2RAssert.HasAsyncVariant(reader, "ValueTaskMethod"); + } } /// @@ -154,21 +212,34 @@ public void RuntimeAsyncMethodEmission() [Fact] public void RuntimeAsyncContinuationLayout() { - new R2RTestRunner().Run(new R2RTestCase + var runtimeAsyncContinuationLayout = new CompiledAssembly { - Name = "RuntimeAsyncContinuationLayout", - MainSourceResourceName = "RuntimeAsync/AsyncWithContinuation.cs", - MainExtraSourceResourceNames = ["RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"], + AssemblyName = nameof(RuntimeAsyncContinuationLayout), + SourceResourceNames = + [ + "RuntimeAsync/AsyncWithContinuation.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], Features = { RuntimeAsyncFeature }, - Dependencies = new List(), - Validate = reader => - { - R2RAssert.HasAsyncVariant(reader, "CaptureObjectAcrossAwait"); - R2RAssert.HasAsyncVariant(reader, "CaptureMultipleRefsAcrossAwait"); - R2RAssert.HasContinuationLayout(reader); - R2RAssert.HasResumptionStubFixup(reader); - }, - }); + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(RuntimeAsyncContinuationLayout), + [ + new(nameof(RuntimeAsyncContinuationLayout), [new CrossgenAssembly(runtimeAsyncContinuationLayout)]) + { + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasAsyncVariant(reader, "CaptureObjectAcrossAwait"); + R2RAssert.HasAsyncVariant(reader, "CaptureMultipleRefsAcrossAwait"); + R2RAssert.HasContinuationLayout(reader, "CaptureObjectAcrossAwait"); + R2RAssert.HasContinuationLayout(reader, "CaptureMultipleRefsAcrossAwait"); + R2RAssert.HasResumptionStubFixup(reader, "CaptureObjectAcrossAwait"); + } } /// @@ -178,18 +249,30 @@ public void RuntimeAsyncContinuationLayout() [Fact] public void RuntimeAsyncDevirtualize() { - new R2RTestRunner().Run(new R2RTestCase + var runtimeAsyncDevirtualize = new CompiledAssembly { - Name = "RuntimeAsyncDevirtualize", - MainSourceResourceName = "RuntimeAsync/AsyncDevirtualize.cs", - MainExtraSourceResourceNames = ["RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"], + AssemblyName = nameof(RuntimeAsyncDevirtualize), + SourceResourceNames = + [ + "RuntimeAsync/AsyncDevirtualize.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], Features = { RuntimeAsyncFeature }, - Dependencies = new List(), - Validate = reader => - { - R2RAssert.HasAsyncVariant(reader, "GetValueAsync"); - }, - }); + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(RuntimeAsyncDevirtualize), + [ + new(nameof(RuntimeAsyncDevirtualize), [new CrossgenAssembly(runtimeAsyncDevirtualize)]) + { + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasAsyncVariant(reader, "GetValueAsync"); + } } /// @@ -199,19 +282,31 @@ public void RuntimeAsyncDevirtualize() [Fact] public void RuntimeAsyncNoYield() { - new R2RTestRunner().Run(new R2RTestCase + var runtimeAsyncNoYield = new CompiledAssembly { - Name = "RuntimeAsyncNoYield", - MainSourceResourceName = "RuntimeAsync/AsyncNoYield.cs", - MainExtraSourceResourceNames = ["RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"], + AssemblyName = nameof(RuntimeAsyncNoYield), + SourceResourceNames = + [ + "RuntimeAsync/AsyncNoYield.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], Features = { RuntimeAsyncFeature }, - Dependencies = new List(), - Validate = reader => - { - R2RAssert.HasAsyncVariant(reader, "AsyncButNoAwait"); - R2RAssert.HasAsyncVariant(reader, "AsyncWithConditionalAwait"); - }, - }); + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(RuntimeAsyncNoYield), + [ + new(nameof(RuntimeAsyncNoYield), [new CrossgenAssembly(runtimeAsyncNoYield)]) + { + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasAsyncVariant(reader, "AsyncButNoAwait"); + R2RAssert.HasAsyncVariant(reader, "AsyncWithConditionalAwait"); + } } /// @@ -221,32 +316,578 @@ public void RuntimeAsyncNoYield() [Fact] public void RuntimeAsyncCrossModule() { - new R2RTestRunner().Run(new R2RTestCase - { - Name = "RuntimeAsyncCrossModule", - MainSourceResourceName = "RuntimeAsync/AsyncCrossModule.cs", - MainExtraSourceResourceNames = ["RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"], - Features = { RuntimeAsyncFeature }, - Crossgen2Options = { Crossgen2Option.CrossModuleOptimization("AsyncDepLib") }, - Dependencies = new List - { - new() - { - AssemblyName = "AsyncDepLib", - SourceResourceNames = - [ - "RuntimeAsync/Dependencies/AsyncDepLib.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" - ], - IsCrossgenInput = true, - Features = { RuntimeAsyncFeature }, - } - }, - Validate = reader => - { - R2RAssert.HasManifestRef(reader, "AsyncDepLib"); - R2RAssert.HasAsyncVariant(reader, "CallCrossModuleAsync"); - }, - }); + var asyncDepLib = new CompiledAssembly + { + AssemblyName = "AsyncDepLib", + SourceResourceNames = + [ + "RuntimeAsync/Dependencies/AsyncDepLib.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + }; + var runtimeAsyncCrossModule = new CompiledAssembly + { + AssemblyName = nameof(RuntimeAsyncCrossModule), + SourceResourceNames = + [ + "RuntimeAsync/AsyncCrossModule.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + References = [asyncDepLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(RuntimeAsyncCrossModule), + [ + new(nameof(RuntimeAsyncCrossModule), + [ + new CrossgenAssembly(runtimeAsyncCrossModule), + new CrossgenAssembly(asyncDepLib) + { + Kind = Crossgen2InputKind.Reference, + Options = [Crossgen2AssemblyOption.CrossModuleOptimization], + }, + ]) + { + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "AsyncDepLib"); + R2RAssert.HasAsyncVariant(reader, "CallCrossModuleAsync"); + } + } + + // ===================================================================== + // Tier 1: Critical intersection tests + // ===================================================================== + + /// + /// Composite mode with sync cross-module inlining. + /// Validates that InliningInfo2 and CrossModuleInlineInfo sections + /// are properly populated (CompositeBasic only validates ManifestRef). + /// + [Fact] + public void CompositeCrossModuleInlining() + { + var inlineableLib = new CompiledAssembly + { + AssemblyName = "InlineableLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], + }; + var compositeMain = new CompiledAssembly + { + AssemblyName = "CompositeCrossModuleInlining", + SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], + References = [inlineableLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeCrossModuleInlining), + [ + new(nameof(CompositeCrossModuleInlining), + [ + new CrossgenAssembly(inlineableLib), + new CrossgenAssembly(compositeMain), + ]) + { + Options = [Crossgen2Option.Composite], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "InlineableLib"); + } + } + + /// + /// Composite mode with runtime-async methods in both assemblies. + /// Validates async variants exist in composite output. + /// + [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + public void CompositeAsync() + { + var asyncCompositeLib = new CompiledAssembly + { + AssemblyName = "AsyncCompositeLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], + Features = { RuntimeAsyncFeature }, + }; + var compositeAsyncMain = new CompiledAssembly + { + AssemblyName = "CompositeAsyncMain", + SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], + Features = { RuntimeAsyncFeature }, + References = [asyncCompositeLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeAsync), + [ + new(nameof(CompositeAsync), + [ + new CrossgenAssembly(asyncCompositeLib), + new CrossgenAssembly(compositeAsyncMain), + ]) + { + Options = [Crossgen2Option.Composite], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); + R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); + R2RAssert.HasAsyncVariant(reader, "GetValueAsync"); + } + } + + /// + /// The full intersection: composite + runtime-async + cross-module inlining. + /// Async methods from AsyncCompositeLib are inlined into CompositeAsyncMain + /// within a composite image, exercising MutableModule token encoding for + /// cross-module async continuation layouts. + /// + [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + public void CompositeAsyncCrossModuleInlining() + { + var asyncCompositeLib = new CompiledAssembly + { + AssemblyName = "AsyncCompositeLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], + Features = { RuntimeAsyncFeature }, + }; + var compositeAsyncMain = new CompiledAssembly + { + AssemblyName = "CompositeAsyncMain", + SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], + Features = { RuntimeAsyncFeature }, + References = [asyncCompositeLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeAsyncCrossModuleInlining), + [ + new(nameof(CompositeAsyncCrossModuleInlining), + [ + new CrossgenAssembly(asyncCompositeLib), + new CrossgenAssembly(compositeAsyncMain), + ]) + { + Options = [Crossgen2Option.Composite], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); + R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); + R2RAssert.HasInlinedMethod(reader, "CallCompositeAsync", "GetValueAsync"); + R2RAssert.HasContinuationLayout(reader, "CallCompositeAsync"); + } + } + + /// + /// Non-composite runtime-async + cross-module inlining where the inlinee + /// captures GC refs across await points. Validates that ContinuationLayout + /// fixups correctly reference cross-module types via MutableModule tokens. + /// + [Fact] + public void AsyncCrossModuleContinuation() + { + var asyncDepLibCont = new CompiledAssembly + { + AssemblyName = "AsyncDepLibContinuation", + SourceResourceNames = + [ + "RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + }; + var asyncCrossModuleCont = new CompiledAssembly + { + AssemblyName = nameof(AsyncCrossModuleContinuation), + SourceResourceNames = + [ + "RuntimeAsync/AsyncCrossModuleContinuation.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + References = [asyncDepLibCont] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(AsyncCrossModuleContinuation), + [ + new(nameof(AsyncCrossModuleContinuation), + [ + new CrossgenAssembly(asyncCrossModuleCont), + new CrossgenAssembly(asyncDepLibCont) + { + Kind = Crossgen2InputKind.Reference, + Options = [Crossgen2AssemblyOption.CrossModuleOptimization], + }, + ]) + { + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation"); + R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef"); + R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureArray"); + } + } + + /// + /// Two-step compilation: composite A+B, then non-composite C referencing A+B. + /// Exercises the multi-compilation model. + /// + [Fact] + public void MultiStepCompositeAndNonComposite() + { + var libA = new CompiledAssembly + { + AssemblyName = "MultiStepLibA", + SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepLibA.cs"], + }; + var libB = new CompiledAssembly + { + AssemblyName = "MultiStepLibB", + SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepLibB.cs"], + References = [libA] + }; + var consumer = new CompiledAssembly + { + AssemblyName = "MultiStepConsumer", + SourceResourceNames = ["CrossModuleInlining/MultiStepConsumer.cs"], + References = [libA] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(MultiStepCompositeAndNonComposite), + [ + new("CompositeStep", + [ + new CrossgenAssembly(libA), + new CrossgenAssembly(libB), + ]) + { + Options = [Crossgen2Option.Composite], + Validate = reader => + { + R2RAssert.HasManifestRef(reader, "MultiStepLibA"); + }, + }, + new("NonCompositeStep", + [ + new CrossgenAssembly(consumer), + new CrossgenAssembly(libA) + { + Kind = Crossgen2InputKind.Reference, + Options = [Crossgen2AssemblyOption.CrossModuleOptimization], + }, + ]) + { + Validate = reader => + { + R2RAssert.HasManifestRef(reader, "MultiStepLibA"); + R2RAssert.HasCrossModuleInlinedMethod(reader, "GetValueFromLibA", "GetValue"); + }, + }, + ])); + } + + // ===================================================================== + // Tier 2: Depth coverage + // ===================================================================== + + /// + /// Composite + runtime-async + cross-module devirtualization. + /// Interface defined in AsyncInterfaceLib, call sites in CompositeAsyncDevirtMain. + /// + [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + public void CompositeAsyncDevirtualize() + { + var asyncInterfaceLib = new CompiledAssembly + { + AssemblyName = "AsyncInterfaceLib", + SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncInterfaceLib.cs"], + Features = { RuntimeAsyncFeature }, + }; + var compositeDevirtMain = new CompiledAssembly + { + AssemblyName = "CompositeAsyncDevirtMain", + SourceResourceNames = + [ + "RuntimeAsync/CompositeAsyncDevirtMain.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + References = [asyncInterfaceLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeAsyncDevirtualize), + [ + new(nameof(CompositeAsyncDevirtualize), + [ + new CrossgenAssembly(asyncInterfaceLib), + new CrossgenAssembly(compositeDevirtMain), + ]) + { + Options = [Crossgen2Option.Composite], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "AsyncInterfaceLib"); + R2RAssert.HasAsyncVariant(reader, "CallOnSealed"); + } + } + + /// + /// Composite with 3 assemblies in A→B→C transitive chain. + /// Validates manifest refs for all three and transitive inlining. + /// + [Fact] + public void CompositeTransitive() + { + var externalLib = new CompiledAssembly + { + AssemblyName = "ExternalLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/ExternalLib.cs"], + }; + var inlineableLibTransitive = new CompiledAssembly + { + AssemblyName = "InlineableLibTransitive", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLibTransitive.cs"], + References = [externalLib] + }; + var compositeTransitiveMain = new CompiledAssembly + { + AssemblyName = "CompositeTransitive", + SourceResourceNames = ["CrossModuleInlining/TransitiveReferences.cs"], + References = [inlineableLibTransitive, externalLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeTransitive), + [ + new(nameof(CompositeTransitive), + [ + new CrossgenAssembly(externalLib), + new CrossgenAssembly(inlineableLibTransitive), + new CrossgenAssembly(compositeTransitiveMain), + ]) + { + Options = [Crossgen2Option.Composite], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "InlineableLibTransitive"); + R2RAssert.HasManifestRef(reader, "ExternalLib"); + } + } + + /// + /// Non-composite runtime-async + transitive cross-module inlining. + /// Chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. + /// + [Fact] + public void AsyncCrossModuleTransitive() + { + var asyncExternalLib = new CompiledAssembly + { + AssemblyName = "AsyncExternalLib", + SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncExternalLib.cs"], + }; + var asyncTransitiveLib = new CompiledAssembly + { + AssemblyName = "AsyncTransitiveLib", + SourceResourceNames = + [ + "RuntimeAsync/Dependencies/AsyncTransitiveLib.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + References = [asyncExternalLib] + }; + var asyncTransitiveMain = new CompiledAssembly + { + AssemblyName = nameof(AsyncCrossModuleTransitive), + SourceResourceNames = + [ + "RuntimeAsync/AsyncTransitiveMain.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + References = [asyncTransitiveLib, asyncExternalLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(AsyncCrossModuleTransitive), + [ + new(nameof(AsyncCrossModuleTransitive), + [ + new CrossgenAssembly(asyncTransitiveMain), + new CrossgenAssembly(asyncExternalLib) { Kind = Crossgen2InputKind.Reference }, + new CrossgenAssembly(asyncTransitiveLib) + { + Kind = Crossgen2InputKind.Reference, + Options = [Crossgen2AssemblyOption.CrossModuleOptimization], + }, + ]) + { + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib"); + R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync"); + } + } + + /// + /// Composite + runtime-async + transitive (3 assemblies). + /// Full combination of composite, async, and transitive references. + /// + [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + public void CompositeAsyncTransitive() + { + var asyncExternalLib = new CompiledAssembly + { + AssemblyName = "AsyncExternalLib", + SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncExternalLib.cs"], + }; + var asyncTransitiveLib = new CompiledAssembly + { + AssemblyName = "AsyncTransitiveLib", + SourceResourceNames = + [ + "RuntimeAsync/Dependencies/AsyncTransitiveLib.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + References = [asyncExternalLib] + }; + var compositeAsyncTransitiveMain = new CompiledAssembly + { + AssemblyName = "CompositeAsyncTransitive", + SourceResourceNames = + [ + "RuntimeAsync/AsyncTransitiveMain.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + References = [asyncTransitiveLib, asyncExternalLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeAsyncTransitive), + [ + new(nameof(CompositeAsyncTransitive), + [ + new CrossgenAssembly(asyncExternalLib), + new CrossgenAssembly(asyncTransitiveLib), + new CrossgenAssembly(compositeAsyncTransitiveMain), + ]) + { + Options = [Crossgen2Option.Composite], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib"); + R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync"); + } + } + + /// + /// Multi-step compilation with runtime-async in all assemblies. + /// Step 1: Composite of async libs. Step 2: Non-composite consumer + /// with cross-module inlining of async methods. + /// + [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + public void MultiStepCompositeAndNonCompositeAsync() + { + var asyncCompositeLib = new CompiledAssembly + { + AssemblyName = "AsyncCompositeLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], + Features = { RuntimeAsyncFeature }, + }; + var compositeAsyncMain = new CompiledAssembly + { + AssemblyName = "CompositeAsyncMain", + SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], + Features = { RuntimeAsyncFeature }, + References = [asyncCompositeLib] + }; + var asyncConsumer = new CompiledAssembly + { + AssemblyName = "MultiStepAsyncConsumer", + SourceResourceNames = + [ + "RuntimeAsync/AsyncCrossModuleContinuation.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + References = [asyncCompositeLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(MultiStepCompositeAndNonCompositeAsync), + [ + new("CompositeAsyncStep", + [ + new CrossgenAssembly(asyncCompositeLib), + new CrossgenAssembly(compositeAsyncMain), + ]) + { + Options = [Crossgen2Option.Composite], + Validate = reader => + { + R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); + R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); + }, + }, + new("NonCompositeAsyncStep", + [ + new CrossgenAssembly(asyncConsumer), + new CrossgenAssembly(asyncCompositeLib) + { + Kind = Crossgen2InputKind.Reference, + Options = [Crossgen2AssemblyOption.CrossModuleOptimization], + }, + ]) + { + Validate = reader => + { + R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); + R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef"); + }, + }, + ])); } } From 805f042c6cbd1689cc45f6a3d5002c59efc2fb5d Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:11:05 -0700 Subject: [PATCH 19/74] Add test source files for new R2R test cases Add C# source files for CompositeAsync, MultiStepConsumer, AsyncCrossModuleContinuation, AsyncTransitiveMain, CompositeAsyncDevirt test cases and their dependencies. Add CrossModuleInliningInfoSection parser for ILCompiler.Reflection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CrossModuleInlining/CompositeAsync.cs | 26 ++ .../Dependencies/AsyncCompositeLib.cs | 25 ++ .../Dependencies/MultiStepLibA.cs | 13 + .../Dependencies/MultiStepLibB.cs | 13 + .../CrossModuleInlining/MultiStepConsumer.cs | 20 + .../AsyncCrossModuleContinuation.cs | 21 ++ .../RuntimeAsync/AsyncTransitiveMain.cs | 21 ++ .../RuntimeAsync/CompositeAsyncDevirtMain.cs | 22 ++ .../Dependencies/AsyncDepLibContinuation.cs | 27 ++ .../Dependencies/AsyncExternalLib.cs | 13 + .../Dependencies/AsyncInterfaceLib.cs | 30 ++ .../Dependencies/AsyncTransitiveLib.cs | 23 ++ .../CrossModuleInliningInfoSection.cs | 346 ++++++++++++++++++ 13 files changed, 600 insertions(+) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs new file mode 100644 index 00000000000000..7e19cc7eea200c --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs @@ -0,0 +1,26 @@ +// Test: Composite mode with runtime-async methods across assemblies. +// Validates that async methods produce [ASYNC] variants in composite output. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class CompositeAsyncMain +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCompositeAsync() + { + return await AsyncCompositeLib.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCompositeStringAsync() + { + return await AsyncCompositeLib.GetStringAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CallCompositeSync() + { + return AsyncCompositeLib.GetValueSync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs new file mode 100644 index 00000000000000..bece15b74b11b6 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs @@ -0,0 +1,25 @@ +// Dependency library for composite async tests. +// Contains runtime-async methods called from another assembly in composite mode. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncCompositeLib +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetValueAsync() + { + await Task.Yield(); + return 42; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetStringAsync() + { + await Task.Yield(); + return "composite_async"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetValueSync() => 99; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs new file mode 100644 index 00000000000000..c47dd6ac274f28 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs @@ -0,0 +1,13 @@ +// Dependency library for multi-step compilation tests. +// Contains sync inlineable methods used in both composite and non-composite steps. +using System; +using System.Runtime.CompilerServices; + +public static class MultiStepLibA +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetValue() => 42; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetLabel() => "LibA"; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs new file mode 100644 index 00000000000000..cf8acd54d8bd9b --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs @@ -0,0 +1,13 @@ +// Second library for multi-step composite compilation. +// Compiled together with MultiStepLibA as a composite in step 1. +using System; +using System.Runtime.CompilerServices; + +public static class MultiStepLibB +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetCompositeValue() => MultiStepLibA.GetValue() + 1; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetCompositeLabel() => MultiStepLibA.GetLabel() + "_B"; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs new file mode 100644 index 00000000000000..ec1264f22f1380 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs @@ -0,0 +1,20 @@ +// Test: Non-composite consumer of assemblies that were also compiled as composite. +// Step 1 compiles MultiStepLibA + MultiStepLibB as composite. +// Step 2 compiles this assembly non-composite with --ref to LibA and --opt-cross-module. +using System; +using System.Runtime.CompilerServices; + +public static class MultiStepConsumer +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static int GetValueFromLibA() + { + return MultiStepLibA.GetValue(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static string GetLabelFromLibA() + { + return MultiStepLibA.GetLabel(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs new file mode 100644 index 00000000000000..d22d7c76c881b0 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs @@ -0,0 +1,21 @@ +// Test: Non-composite runtime-async cross-module inlining with continuation layouts. +// The inlinee methods capture GC refs across await points, which forces +// ContinuationLayout fixups that reference cross-module types. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncCrossModuleContinuation +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCrossModuleCaptureRef() + { + return await AsyncDepLibContinuation.CaptureRefAcrossAwait(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCrossModuleCaptureArray() + { + return await AsyncDepLibContinuation.CaptureArrayAcrossAwait(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs new file mode 100644 index 00000000000000..e92cd6f1ae2de4 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs @@ -0,0 +1,21 @@ +// Test: Non-composite runtime-async transitive cross-module inlining. +// Chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. +// Validates transitive manifest refs and async cross-module inlining. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncTransitiveMain +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallTransitiveValueAsync() + { + return await AsyncTransitiveLib.GetExternalValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallTransitiveLabelAsync() + { + return await AsyncTransitiveLib.GetExternalLabelAsync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs new file mode 100644 index 00000000000000..62a06bc816f0c2 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs @@ -0,0 +1,22 @@ +// Test: Composite mode async devirtualization across module boundaries. +// Interface defined in AsyncInterfaceLib, call sites here. +// In composite mode, crossgen2 should devirtualize sealed type dispatch. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class CompositeAsyncDevirtMain +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallOnSealed(SealedAsyncService svc) + { + return await svc.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallOnNewOpen() + { + IAsyncCompositeService svc = new OpenAsyncService(); + return await svc.GetValueAsync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs new file mode 100644 index 00000000000000..b4124b80008fe1 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs @@ -0,0 +1,27 @@ +// Dependency library for async cross-module continuation tests. +// Contains runtime-async methods that capture GC refs across await points, +// forcing ContinuationLayout fixup emission when cross-module inlined. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncDepLibContinuation +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task CaptureRefAcrossAwait() + { + object o = new object(); + string s = "cross_module"; + await Task.Yield(); + return s + o.GetHashCode(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task CaptureArrayAcrossAwait() + { + int[] arr = new int[] { 10, 20, 30 }; + string label = "sum"; + await Task.Yield(); + return arr[0] + arr[1] + label.Length; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs new file mode 100644 index 00000000000000..f45db23f004570 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs @@ -0,0 +1,13 @@ +// External types for async transitive cross-module tests. +// Similar to ExternalLib but with async-friendly types. +using System; + +public static class AsyncExternalLib +{ + public static int ExternalValue => 77; + + public class AsyncExternalType + { + public string Label { get; set; } = "external"; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs new file mode 100644 index 00000000000000..528162b5d10c10 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs @@ -0,0 +1,30 @@ +// Dependency library: defines an async interface and sealed implementation +// for cross-module async devirtualization tests in composite mode. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public interface IAsyncCompositeService +{ + Task GetValueAsync(); +} + +public sealed class SealedAsyncService : IAsyncCompositeService +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task GetValueAsync() + { + await Task.Yield(); + return 42; + } +} + +public class OpenAsyncService : IAsyncCompositeService +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public virtual async Task GetValueAsync() + { + await Task.Yield(); + return 10; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs new file mode 100644 index 00000000000000..717040a417c362 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs @@ -0,0 +1,23 @@ +// Middle library in async transitive chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. +// Contains runtime-async methods that reference types from AsyncExternalLib. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncTransitiveLib +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetExternalValueAsync() + { + await Task.Yield(); + return AsyncExternalLib.ExternalValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetExternalLabelAsync() + { + var ext = new AsyncExternalLib.AsyncExternalType(); + await Task.Yield(); + return ext.Label; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs new file mode 100644 index 00000000000000..e690a5676c8108 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs @@ -0,0 +1,346 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Text; +using Internal.ReadyToRunConstants; + +namespace ILCompiler.Reflection.ReadyToRun +{ + /// + /// Parser for the CrossModuleInlineInfo section (ReadyToRunSectionType 119, added in R2R v6.3). + /// This format differs from InliningInfo2 (section 114) — it uses a stream-size counted + /// encoding with 2-bit flags on the inlinee index and supports ILBody import indices for + /// cross-module inlinees and inliners. + /// + public class CrossModuleInliningInfoSection + { + public enum InlineeReferenceKind + { + Local, + CrossModule, + } + + private enum CrossModuleInlineFlags : uint + { + CrossModuleInlinee = 0x1, + HasCrossModuleInliners = 0x2, + CrossModuleInlinerIndexShift = 2, + InlinerRidHasModule = 0x1, + InlinerRidShift = 1, + } + + /// + /// Identifies a method in the inlining info section. + /// For cross-module methods, Index is an ILBody import section index. + /// For local methods, Index is a MethodDef RID and ModuleIndex identifies the owning module. + /// + public readonly struct MethodRef + { + public bool IsCrossModule { get; } + public uint Index { get; } + public uint ModuleIndex { get; } + + public MethodRef(bool isCrossModule, uint index, uint moduleIndex = 0) + { + IsCrossModule = isCrossModule; + Index = index; + ModuleIndex = moduleIndex; + } + } + + /// + /// A single inlinee and all the methods that inline it. + /// + public readonly struct InliningEntry + { + public MethodRef Inlinee { get; } + public IReadOnlyList Inliners { get; } + + public InliningEntry(MethodRef inlinee, IReadOnlyList inliners) + { + Inlinee = inlinee; + Inliners = inliners; + } + } + + private readonly ReadyToRunReader _r2r; + private readonly int _startOffset; + private readonly int _endOffset; + private readonly bool _multiModuleFormat; + + public CrossModuleInliningInfoSection(ReadyToRunReader reader, int offset, int endOffset) + { + _r2r = reader; + _startOffset = offset; + _endOffset = endOffset; + _multiModuleFormat = (reader.ReadyToRunHeader.Flags & (uint)ReadyToRunFlags.READYTORUN_FLAG_MultiModuleVersionBubble) != 0; + } + + /// + /// Parses the section into structured inlining entries. + /// + public List GetEntries() + { + var entries = new List(); + + NativeParser parser = new NativeParser(_r2r.ImageReader, (uint)_startOffset); + NativeHashtable hashtable = new NativeHashtable(_r2r.ImageReader, parser, (uint)_endOffset); + + var enumerator = hashtable.EnumerateAllEntries(); + NativeParser curParser = enumerator.GetNext(); + while (!curParser.IsNull()) + { + uint streamSize = curParser.GetUnsigned(); + uint inlineeIndexAndFlags = curParser.GetUnsigned(); + streamSize--; + + uint inlineeIndex = inlineeIndexAndFlags >> (int)CrossModuleInlineFlags.CrossModuleInlinerIndexShift; + bool hasCrossModuleInliners = (inlineeIndexAndFlags & (uint)CrossModuleInlineFlags.HasCrossModuleInliners) != 0; + bool crossModuleInlinee = (inlineeIndexAndFlags & (uint)CrossModuleInlineFlags.CrossModuleInlinee) != 0; + + MethodRef inlinee; + if (crossModuleInlinee) + { + inlinee = new MethodRef(isCrossModule: true, index: inlineeIndex); + } + else + { + uint moduleIndex = 0; + if (_multiModuleFormat && streamSize > 0) + { + moduleIndex = curParser.GetUnsigned(); + streamSize--; + } + inlinee = new MethodRef(isCrossModule: false, index: inlineeIndex, moduleIndex: moduleIndex); + } + + var inliners = new List(); + + if (hasCrossModuleInliners && streamSize > 0) + { + uint crossModuleInlinerCount = curParser.GetUnsigned(); + streamSize--; + + uint baseIndex = 0; + for (uint i = 0; i < crossModuleInlinerCount && streamSize > 0; i++) + { + uint inlinerDelta = curParser.GetUnsigned(); + streamSize--; + baseIndex += inlinerDelta; + inliners.Add(new MethodRef(isCrossModule: true, index: baseIndex)); + } + } + + uint currentRid = 0; + while (streamSize > 0) + { + uint inlinerDeltaAndFlag = curParser.GetUnsigned(); + streamSize--; + + uint moduleIndex = 0; + if (_multiModuleFormat) + { + currentRid += inlinerDeltaAndFlag >> (int)CrossModuleInlineFlags.InlinerRidShift; + if ((inlinerDeltaAndFlag & (uint)CrossModuleInlineFlags.InlinerRidHasModule) != 0 && streamSize > 0) + { + moduleIndex = curParser.GetUnsigned(); + streamSize--; + } + } + else + { + currentRid += inlinerDeltaAndFlag; + } + inliners.Add(new MethodRef(isCrossModule: false, index: currentRid, moduleIndex: moduleIndex)); + } + + entries.Add(new InliningEntry(inlinee, inliners)); + curParser = enumerator.GetNext(); + } + + return entries; + } + + /// + /// Resolves a to a human-readable method name. + /// Cross-module methods are resolved via the ILBody import section entries. + /// Local methods are resolved via the R2R reader's compiled method table. + /// + public string ResolveMethodName(MethodRef methodRef) + { + if (methodRef.IsCrossModule) + { + return ResolveCrossModuleMethod(methodRef.Index); + } + + return ResolveLocalMethod(methodRef.Index); + } + + /// + /// Returns all inlining pairs with resolved method names and whether the inlinee is cross-module. + /// + public IEnumerable<(string InlinerName, string InlineeName, InlineeReferenceKind InlineeKind)> GetInliningPairs() + { + foreach (var entry in GetEntries()) + { + string inlineeName = ResolveMethodName(entry.Inlinee); + var kind = entry.Inlinee.IsCrossModule ? InlineeReferenceKind.CrossModule : InlineeReferenceKind.Local; + foreach (var inliner in entry.Inliners) + { + yield return (ResolveMethodName(inliner), inlineeName, kind); + } + } + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + foreach (var entry in GetEntries()) + { + if (entry.Inlinee.IsCrossModule) + { + sb.AppendLine($"Inliners for cross-module inlinee (ILBody import index {entry.Inlinee.Index}):"); + } + else + { + int token = RidToMethodDef((int)entry.Inlinee.Index); + if (_multiModuleFormat) + { + string moduleName = TryGetModuleName(entry.Inlinee.ModuleIndex); + sb.AppendLine($"Inliners for inlinee {token:X8} (module {moduleName}):"); + } + else + { + sb.AppendLine($"Inliners for inlinee {token:X8}:"); + } + } + + foreach (var inliner in entry.Inliners) + { + if (inliner.IsCrossModule) + { + sb.AppendLine($" cross-module inliner (ILBody import index {inliner.Index})"); + } + else + { + int token = RidToMethodDef((int)inliner.Index); + if (inliner.ModuleIndex != 0 || _multiModuleFormat) + { + string moduleName = TryGetModuleName(inliner.ModuleIndex); + sb.AppendLine($" {token:X8} (module {moduleName})"); + } + else + { + sb.AppendLine($" {token:X8}"); + } + } + } + } + + return sb.ToString(); + } + + private string ResolveCrossModuleMethod(uint importIndex) + { + if (!_ilBodyImportSectionResolved) + { + _ilBodyImportSection = FindILBodyImportSection(); + _ilBodyImportSectionResolved = true; + } + + if (_ilBodyImportSection.Entries is not null && importIndex < (uint)_ilBodyImportSection.Entries.Count) + { + var entry = _ilBodyImportSection.Entries[(int)importIndex]; + if (entry.Signature is not null) + { + string sig = entry.Signature.ToString(new SignatureFormattingOptions()); + int parenIdx = sig.LastIndexOf(" (", StringComparison.Ordinal); + + return parenIdx >= 0 ? sig[..parenIdx] : sig; + } + } + + return $""; + } + + private string ResolveLocalMethod(uint rid) + { + _localMethodMap ??= BuildLocalMethodMap(); + string name; + + return _localMethodMap.TryGetValue(rid, out name) + ? name + : $""; + } + + private ReadyToRunImportSection FindILBodyImportSection() + { + foreach (var section in _r2r.ImportSections) + { + foreach (var entry in section.Entries) + { + if (entry.Signature?.FixupKind is ReadyToRunFixupKind.Check_IL_Body or ReadyToRunFixupKind.Verify_IL_Body) + return section; + } + } + + return default; + } + + private Dictionary BuildLocalMethodMap() + { + var map = new Dictionary(); + foreach (var assembly in _r2r.ReadyToRunAssemblies) + { + foreach (var method in assembly.Methods) + { + if (method.MethodHandle.Kind == HandleKind.MethodDefinition) + { + uint methodRid = (uint)MetadataTokens.GetRowNumber((MethodDefinitionHandle)method.MethodHandle); + map[methodRid] = method.SignatureString; + } + } + } + + foreach (var instanceEntry in _r2r.InstanceMethods) + { + if (instanceEntry.Method.MethodHandle.Kind == HandleKind.MethodDefinition) + { + uint methodRid = (uint)MetadataTokens.GetRowNumber((MethodDefinitionHandle)instanceEntry.Method.MethodHandle); + map.TryAdd(methodRid, instanceEntry.Method.SignatureString); + } + } + + return map; + } + + private ReadyToRunImportSection _ilBodyImportSection; + private bool _ilBodyImportSectionResolved; + private Dictionary _localMethodMap; + + private string TryGetModuleName(uint moduleIndex) + { + if (moduleIndex == 0 && !_r2r.Composite) + { + return Path.GetFileNameWithoutExtension(_r2r.Filename); + } + + try + { + return _r2r.GetReferenceAssemblyName((int)moduleIndex); + } + catch + { + return $""; + } + } + + static int RidToMethodDef(int rid) => MetadataTokens.GetToken(MetadataTokens.MethodDefinitionHandle(rid)); + } +} From d90a6cd5c26ad8bedb1b02ffe2e3447bce6fe787 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:21:12 -0700 Subject: [PATCH 20/74] Fix composite tests: add --optimize flag and cross-assembly inline assertion In composite mode, crossgen2 skips the DebuggableAttribute-based optimization detection (Program.cs:588), defaulting to OptimizationMode.None which sets CORJIT_FLAG_DEBUG_CODE and disables all inlining. Add Crossgen2Option.Optimize to all composite compilations. Add HasInlinedMethod assertion to CompositeCrossModuleInlining test to verify cross-assembly inlining actually occurs in composite mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 9ce6b95f4513b7..c48936aae0ab1a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -162,7 +162,7 @@ public void CompositeBasic() new CrossgenAssembly(compositeBasic), ]) { - Options = [Crossgen2Option.Composite], + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = Validate, }, ])); @@ -395,7 +395,7 @@ public void CompositeCrossModuleInlining() new CrossgenAssembly(compositeMain), ]) { - Options = [Crossgen2Option.Composite], + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = Validate, }, ])); @@ -403,6 +403,7 @@ public void CompositeCrossModuleInlining() static void Validate(ReadyToRunReader reader) { R2RAssert.HasManifestRef(reader, "InlineableLib"); + R2RAssert.HasInlinedMethod(reader, "TestGetValue", "GetValue"); } } @@ -436,7 +437,7 @@ public void CompositeAsync() new CrossgenAssembly(compositeAsyncMain), ]) { - Options = [Crossgen2Option.Composite], + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = Validate, }, ])); @@ -481,7 +482,7 @@ public void CompositeAsyncCrossModuleInlining() new CrossgenAssembly(compositeAsyncMain), ]) { - Options = [Crossgen2Option.Composite], + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = Validate, }, ])); @@ -584,7 +585,7 @@ public void MultiStepCompositeAndNonComposite() new CrossgenAssembly(libB), ]) { - Options = [Crossgen2Option.Composite], + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = reader => { R2RAssert.HasManifestRef(reader, "MultiStepLibA"); @@ -647,7 +648,7 @@ public void CompositeAsyncDevirtualize() new CrossgenAssembly(compositeDevirtMain), ]) { - Options = [Crossgen2Option.Composite], + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = Validate, }, ])); @@ -694,7 +695,7 @@ public void CompositeTransitive() new CrossgenAssembly(compositeTransitiveMain), ]) { - Options = [Crossgen2Option.Composite], + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = Validate, }, ])); @@ -811,7 +812,7 @@ public void CompositeAsyncTransitive() new CrossgenAssembly(compositeAsyncTransitiveMain), ]) { - Options = [Crossgen2Option.Composite], + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = Validate, }, ])); @@ -865,7 +866,7 @@ public void MultiStepCompositeAndNonCompositeAsync() new CrossgenAssembly(compositeAsyncMain), ]) { - Options = [Crossgen2Option.Composite], + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = reader => { R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); From 67e8d19664e425d9facf55e15789a5a420325644 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:25:29 -0700 Subject: [PATCH 21/74] Fix code review issues: parser bug, deadlock, file handle leak - CrossModuleInliningInfoSection: Fix cross-module inliner index parsing. The writer emits absolute ILBody indices (baseIndex is never updated), and the runtime reads them as absolute. The parser was incorrectly accumulating them as deltas, which would break for inlinees with 2+ cross-module inliners. - R2RDriver: Read stdout/stderr concurrently via ReadToEndAsync to avoid the classic redirected-stdio deadlock when crossgen2 fills one pipe buffer while we block reading the other. - R2RResultChecker: Load PE images into memory via File.ReadAllBytes instead of holding FileStream handles open. IAssemblyMetadata has no disposal contract and ReadyToRunReader caches instances indefinitely, so stream-backed implementations leak file handles. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCasesRunner/R2RDriver.cs | 10 ++++++++-- .../TestCasesRunner/R2RResultChecker.cs | 17 ++++++----------- .../CrossModuleInliningInfoSection.cs | 8 ++++---- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs index 7b4fe7b11fb491..b61f978b3112e8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs @@ -121,8 +121,11 @@ private R2RCompilationResult RunCrossgen2(List crossgen2Args) } using var process = Process.Start(psi)!; - string stdout = process.StandardOutput.ReadToEnd(); - string stderr = process.StandardError.ReadToEnd(); + + // Read stdout and stderr concurrently to avoid pipe buffer deadlock. + // If crossgen2 fills one pipe while we block reading the other, both processes hang. + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); if (!process.WaitForExit(ProcessTimeout)) { @@ -131,6 +134,9 @@ private R2RCompilationResult RunCrossgen2(List crossgen2Args) throw new TimeoutException($"crossgen2 timed out after {ProcessTimeout.TotalMinutes} minutes"); } + string stdout = stdoutTask.GetAwaiter().GetResult(); + string stderr = stderrTask.GetAwaiter().GetResult(); + if (process.ExitCode != 0) { _output.WriteLine($" crossgen2 FAILED (exit code {process.ExitCode})"); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index 2cbd2ecb938b97..e182a74e728d23 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -349,26 +349,21 @@ internal sealed class SimpleAssemblyResolver : IAssemblyResolver } /// -/// Simple assembly metadata wrapper. +/// Simple assembly metadata wrapper that loads the PE image into memory +/// to avoid holding file handles open (IAssemblyMetadata has no disposal contract, +/// and ReadyToRunReader caches these indefinitely). /// -internal sealed class SimpleAssemblyMetadata : IAssemblyMetadata, IDisposable +internal sealed class SimpleAssemblyMetadata : IAssemblyMetadata { - private readonly FileStream _stream; private readonly PEReader _peReader; public SimpleAssemblyMetadata(string path) { - _stream = File.OpenRead(path); - _peReader = new PEReader(_stream); + byte[] imageBytes = File.ReadAllBytes(path); + _peReader = new PEReader(new MemoryStream(imageBytes)); } public PEReader ImageReader => _peReader; public MetadataReader MetadataReader => _peReader.GetMetadataReader(); - - public void Dispose() - { - _peReader.Dispose(); - _stream.Dispose(); - } } diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs index e690a5676c8108..b19adfc4e4be56 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs @@ -127,13 +127,13 @@ public List GetEntries() uint crossModuleInlinerCount = curParser.GetUnsigned(); streamSize--; - uint baseIndex = 0; + // Cross-module inliner indices are absolute ILBody import indices, + // not delta-encoded (the writer never updates baseIndex between entries). for (uint i = 0; i < crossModuleInlinerCount && streamSize > 0; i++) { - uint inlinerDelta = curParser.GetUnsigned(); + uint inlinerIndex = curParser.GetUnsigned(); streamSize--; - baseIndex += inlinerDelta; - inliners.Add(new MethodRef(isCrossModule: true, index: baseIndex)); + inliners.Add(new MethodRef(isCrossModule: true, index: inlinerIndex)); } } From 2ab55e0088121f8f1780fbc5d6ef62e69fd66af3 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:17:21 -0700 Subject: [PATCH 22/74] Add CrossModuleGenericMultiInliner test for cross-module inliner parsing Add a test that exercises the cross-module INLINER parsing path in CrossModuleInliningInfoSection, where multiple cross-module inliner entries exist for the same inlinee. The test uses two generic types (GenericWrapperA, GenericWrapperB) from a --opt-cross-module reference library, each with a virtual method that inlines the same Utility.GetValue(). Value type instantiations (LocalStruct) are required because CrossModuleCompileable discovery uses CanonicalFormKind.Specific, which preserves value types (unlike reference types that canonicalize to __Canon, losing alternate location info). This produces two distinct cross-module inliner MethodDefs for the same inlinee, validating that the parser correctly reads absolute (not delta-accumulated) inliner indices. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Dependencies/CrossModuleGenericLib.cs | 42 +++++++++++++++ .../MultiInlinerConsumer.cs | 28 ++++++++++ .../TestCases/R2RTestSuites.cs | 52 +++++++++++++++++++ .../TestCasesRunner/R2RResultChecker.cs | 41 +++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs new file mode 100644 index 00000000000000..c3c4ebcb2c2d04 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs @@ -0,0 +1,42 @@ +using System.Runtime.CompilerServices; + +/// +/// Library with two generic types that each inline the same utility method. +/// When compiled via CrossModuleCompileable generics, each type's InvokeGetValue() +/// becomes a distinct cross-module inliner MethodDef for the same inlinee (Utility.GetValue), +/// producing multiple cross-module inliner entries in the CrossModuleInlineInfo section. +/// +public static class Utility +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetValue() => 42; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetName() => "hello"; +} + +public class GenericWrapperA +{ + private T _value; + + public GenericWrapperA(T value) => _value = value; + + [MethodImpl(MethodImplOptions.NoInlining)] + public virtual int InvokeGetValue() + { + return Utility.GetValue(); + } +} + +public class GenericWrapperB +{ + private T _value; + + public GenericWrapperB(T value) => _value = value; + + [MethodImpl(MethodImplOptions.NoInlining)] + public virtual int InvokeGetValue() + { + return Utility.GetValue(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs new file mode 100644 index 00000000000000..56e9c37d33b7f5 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs @@ -0,0 +1,28 @@ +/// +/// Consumer that uses two different generic types from CrossModuleGenericLib, +/// each instantiated with a value type defined in this assembly. +/// Value types are required because CrossModuleCompileable discovery uses +/// CanonicalFormKind.Specific, which preserves value type arguments (unlike +/// reference types which become __Canon, losing the alternate location info). +/// +/// GenericWrapperA<LocalStruct>.InvokeGetValue() and GenericWrapperB<LocalStruct>.InvokeGetValue() +/// are two distinct MethodDefs that each inline Utility.GetValue(), producing +/// multiple cross-module inliner entries for the same inlinee in CrossModuleInlineInfo. +/// + +public struct LocalStruct { public int Value; } + +public static class MultiInlinerConsumer +{ + public static int UseA() + { + var wrapper = new GenericWrapperA(new LocalStruct { Value = 1 }); + return wrapper.InvokeGetValue(); + } + + public static int UseB() + { + var wrapper = new GenericWrapperB(new LocalStruct { Value = 2 }); + return wrapper.InvokeGetValue(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index c48936aae0ab1a..d849d9b1f4da14 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -891,4 +891,56 @@ public void MultiStepCompositeAndNonCompositeAsync() }, ])); } + + /// + /// Tests cross-module generic compilation where multiple generic instantiations + /// from an --opt-cross-module library each inline the same utility method. + /// This produces multiple cross-module inliners for the same inlinee in the + /// CrossModuleInlineInfo section, exercising the absolute-index encoding + /// (not delta-encoded) for cross-module inliner entries. + /// + [Fact] + public void CrossModuleGenericMultiInliner() + { + var crossModuleGenericLib = new CompiledAssembly + { + AssemblyName = "CrossModuleGenericLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs"], + }; + var consumer = new CompiledAssembly + { + AssemblyName = "MultiInlinerConsumer", + SourceResourceNames = ["CrossModuleInlining/MultiInlinerConsumer.cs"], + References = [crossModuleGenericLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CrossModuleGenericMultiInliner), + [ + new(consumer.AssemblyName, + [ + new CrossgenAssembly(crossModuleGenericLib) + { + Kind = Crossgen2InputKind.Reference, + Options = [Crossgen2AssemblyOption.CrossModuleOptimization], + }, + new CrossgenAssembly(consumer), + ]) + { + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasManifestRef(reader, "CrossModuleGenericLib"); + R2RAssert.HasCrossModuleInliningInfo(reader); + + // Verify that GetValue has at least 2 cross-module inliners + // (GenericWrapper.InvokeGetValue and GenericWrapper.InvokeGetValue). + // This exercises the cross-module inliner parsing path where indices + // must be read as absolute values, not delta-accumulated. + R2RAssert.HasMultipleCrossModuleInliners(reader, "GetValue", 2); + } + } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index e182a74e728d23..eccb4865f68e91 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -89,6 +89,47 @@ public static void HasCrossModuleInlinedMethod(ReadyToRunReader reader, string i $"Recorded inlining pairs:\n {string.Join("\n ", allPairs)}"); } + /// + /// Asserts that the CrossModuleInlineInfo section has an entry for an inlinee matching + /// with at least cross-module + /// inliners. This exercises the cross-module inliner parsing path where inliner indices + /// are encoded as absolute ILBody import indices. + /// + public static void HasMultipleCrossModuleInliners(ReadyToRunReader reader, string inlineeMethodName, int minCount) + { + var inliningInfo = GetCrossModuleInliningInfoSection(reader); + + foreach (var entry in inliningInfo.GetEntries()) + { + string inlineeName = inliningInfo.ResolveMethodName(entry.Inlinee); + if (!inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) + continue; + + int crossModuleInlinerCount = 0; + var inlinerNames = new List(); + foreach (var inliner in entry.Inliners) + { + string name = inliningInfo.ResolveMethodName(inliner); + inlinerNames.Add($"{name} (crossModule={inliner.IsCrossModule})"); + if (inliner.IsCrossModule) + crossModuleInlinerCount++; + } + + Assert.True(crossModuleInlinerCount >= minCount, + $"Inlinee '{inlineeName}' has {crossModuleInlinerCount} cross-module inliners, " + + $"expected at least {minCount}.\nInliners:\n {string.Join("\n ", inlinerNames)}"); + return; + } + + var allEntries = new List(); + foreach (var (inlinerName, inlineeName, _) in inliningInfo.GetInliningPairs()) + allEntries.Add($"{inlinerName} → {inlineeName}"); + + Assert.Fail( + $"No CrossModuleInlineInfo entry found for inlinee matching '{inlineeMethodName}'.\n" + + $"All inlining pairs:\n {string.Join("\n ", allEntries)}"); + } + /// /// Asserts that any inlining info section (CrossModuleInlineInfo or InliningInfo2) records that /// inlined . From 03d71ec8ed0b21f671fd302c7b4c9903c4944309 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:28:11 -0700 Subject: [PATCH 23/74] Validate resolved inliner names in HasCrossModuleInliners Rename HasMultipleCrossModuleInliners to HasCrossModuleInliners and accept expected inliner method names instead of just a count. This ensures the absolute-encoded ILBody import indices actually resolve to the correct methods (GenericWrapperA, GenericWrapperB). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 8 ++--- .../TestCasesRunner/R2RResultChecker.cs | 30 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index d849d9b1f4da14..f21d18c2db9bab 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -936,11 +936,11 @@ static void Validate(ReadyToRunReader reader) R2RAssert.HasManifestRef(reader, "CrossModuleGenericLib"); R2RAssert.HasCrossModuleInliningInfo(reader); - // Verify that GetValue has at least 2 cross-module inliners - // (GenericWrapper.InvokeGetValue and GenericWrapper.InvokeGetValue). + // Verify that GetValue has cross-module inliners from both GenericWrapperA and GenericWrapperB. // This exercises the cross-module inliner parsing path where indices - // must be read as absolute values, not delta-accumulated. - R2RAssert.HasMultipleCrossModuleInliners(reader, "GetValue", 2); + // must be read as absolute values, not delta-accumulated, and validates + // that the resolved method names match the expected inliners. + R2RAssert.HasCrossModuleInliners(reader, "GetValue", "GenericWrapperA", "GenericWrapperB"); } } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index eccb4865f68e91..975ae479201b52 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -91,11 +91,15 @@ public static void HasCrossModuleInlinedMethod(ReadyToRunReader reader, string i /// /// Asserts that the CrossModuleInlineInfo section has an entry for an inlinee matching - /// with at least cross-module - /// inliners. This exercises the cross-module inliner parsing path where inliner indices - /// are encoded as absolute ILBody import indices. + /// with cross-module inliners whose resolved names + /// contain each of the . This validates that + /// cross-module inliner indices (encoded as absolute ILBody import indices) resolve + /// to the correct method names. /// - public static void HasMultipleCrossModuleInliners(ReadyToRunReader reader, string inlineeMethodName, int minCount) + public static void HasCrossModuleInliners( + ReadyToRunReader reader, + string inlineeMethodName, + params string[] expectedInlinerNames) { var inliningInfo = GetCrossModuleInliningInfoSection(reader); @@ -105,19 +109,21 @@ public static void HasMultipleCrossModuleInliners(ReadyToRunReader reader, strin if (!inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) continue; - int crossModuleInlinerCount = 0; - var inlinerNames = new List(); + var crossModuleInlinerNames = new List(); foreach (var inliner in entry.Inliners) { - string name = inliningInfo.ResolveMethodName(inliner); - inlinerNames.Add($"{name} (crossModule={inliner.IsCrossModule})"); if (inliner.IsCrossModule) - crossModuleInlinerCount++; + crossModuleInlinerNames.Add(inliningInfo.ResolveMethodName(inliner)); + } + + foreach (string expected in expectedInlinerNames) + { + Assert.True( + crossModuleInlinerNames.Any(n => n.Contains(expected, StringComparison.OrdinalIgnoreCase)), + $"Inlinee '{inlineeName}': expected a cross-module inliner matching '{expected}' " + + $"but found only:\n {string.Join("\n ", crossModuleInlinerNames)}"); } - Assert.True(crossModuleInlinerCount >= minCount, - $"Inlinee '{inlineeName}' has {crossModuleInlinerCount} cross-module inliners, " + - $"expected at least {minCount}.\nInliners:\n {string.Join("\n ", inlinerNames)}"); return; } From 22e55bdd1d52d87a25d2eff7c89a1aef70071a45 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:05:39 -0700 Subject: [PATCH 24/74] Remove unused Utility.GetName() from CrossModuleGenericLib test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../plan.md | 118 + r2r-tests-full.diff | 1987 +++++++++++++++++ .../Dependencies/CrossModuleGenericLib.cs | 3 - 3 files changed, 2105 insertions(+), 3 deletions(-) create mode 100644 .copilot/session-state/882aaa64-a620-4614-99b2-c7ab0c584744/plan.md create mode 100644 r2r-tests-full.diff diff --git a/.copilot/session-state/882aaa64-a620-4614-99b2-c7ab0c584744/plan.md b/.copilot/session-state/882aaa64-a620-4614-99b2-c7ab0c584744/plan.md new file mode 100644 index 00000000000000..10ebfff5f0caa8 --- /dev/null +++ b/.copilot/session-state/882aaa64-a620-4614-99b2-c7ab0c584744/plan.md @@ -0,0 +1,118 @@ +# R2R Cross-Module × Async × Composite Test Plan + +## Problem + +We need comprehensive test coverage for the intersection of three features in crossgen2 R2R output: +1. **Cross-module inlining** — methods from one assembly inlined into another +2. **Runtime-async** — async method compilation with [ASYNC] variants, continuation layouts, resumption stubs +3. **Composite mode** — multiple assemblies merged into one R2R image + +Each combination exercises different code paths in crossgen2 (MutableModule token encoding, manifest metadata, inlining info sections, continuation layout fixups) and the runtime (LoadDynamicInfoEntry, assembly loading, fixup resolution). + +## Existing Tests (9, all passing) + +| # | Test | Composite | Cross-Module | Async | Key Validations | +|---|------|-----------|-------------|-------|-----------------| +| 1 | BasicCrossModuleInlining | No | Direct sync | No | ManifestRef, CrossModuleInlinedMethod, CrossModuleInliningInfo | +| 2 | TransitiveReferences | No | Transitive sync | No | ManifestRef×2, CrossModuleInlinedMethod | +| 3 | AsyncCrossModuleInlining | No | Direct (async inlinee) | No¹ | ManifestRef, CrossModuleInlinedMethod | +| 4 | CompositeBasic | Yes | Implicit | No | ManifestRef | +| 5 | RuntimeAsyncMethodEmission | No | None | Yes | AsyncVariant×3 | +| 6 | RuntimeAsyncContinuationLayout | No | None | Yes | AsyncVariant, ContinuationLayout, ResumptionStubFixup | +| 7 | RuntimeAsyncDevirtualize | No | None | Yes | AsyncVariant | +| 8 | RuntimeAsyncNoYield | No | None | Yes | AsyncVariant×2 | +| 9 | RuntimeAsyncCrossModule | No | Direct | Yes | ManifestRef, AsyncVariant | + +¹ AsyncCrossModuleInlining inlines regular async methods (Task-returning) but does NOT use the runtime-async feature flag. + +## Coverage Gaps + +### Gap 1: Composite + detailed inlining validation +CompositeBasic only checks ManifestRef. No test validates that inlining actually occurs between assemblies in composite mode or checks InliningInfo2 / CrossModuleInlineInfo sections. + +### Gap 2: Composite + runtime-async +No test combines `--composite` with the `runtime-async` feature flag. + +### Gap 3: Composite + runtime-async + cross-module inlining +The full intersection — async methods from one assembly inlined into another within a composite image. Exercises MutableModule token encoding for cross-module async continuation layouts in composite metadata. + +### Gap 4: Non-composite runtime-async + cross-module inlining with continuations +RuntimeAsyncCrossModule validates ManifestRef + AsyncVariant but doesn't test --opt-cross-module inlining of async methods with GC refs across await points. + +### Gap 5: Transitive + async +No test combines transitive cross-module references with runtime-async. + +### Gap 6: Multi-step compilation +No test uses the multi-compilation model (compile composite first, then non-composite referencing those assemblies). + +### Gap 7: Composite + async devirtualization +RuntimeAsyncDevirtualize is single-assembly. Cross-module async devirtualization in composite is untested. + +### Gap 8: Input bubble boundaries in composite +No test uses --input-bubble + --inputbubbleref to test version bubble boundaries. + +## New Tests + +### Tier 1: Critical (unique code paths) + +**10. CompositeCrossModuleInlining** — Composite, sync, validates that Lib methods are inlined into Main +- Config: `--composite` +- Validates: ManifestRef, CrossModuleInlineInfo (CrossModuleInliningForCrossModuleDataOnly), InliningInfo2 +- Reuse existing source: BasicInlining.cs + InlineableLib.cs + +**11. CompositeAsync** — Composite + runtime-async baseline +- Config: `--composite`, `runtime-async=on` +- Validates: AsyncVariant for methods in both assemblies, ManifestRef +- New source: AsyncCompositeLib.cs + CompositeAsyncMain.cs + +**12. CompositeAsyncCrossModuleInlining** — THE full intersection test +- Config: `--composite`, `runtime-async=on` +- Validates: AsyncVariant, ContinuationLayout, CrossModuleInlineInfo, ManifestRef +- New source: needs async methods with GC refs across await in cross-module context + +**13. AsyncCrossModuleContinuation** — Non-composite, runtime-async + cross-module + continuations +- Config: `--opt-cross-module`, `runtime-async=on` +- Validates: AsyncVariant, ContinuationLayout, CrossModuleInlinedMethod, ManifestRef +- New source: AsyncDepLibContinuation.cs + AsyncCrossModuleContinuation.cs + +**14. MultiStepCompositeAndNonComposite** — Two-step compilation +- Step 1: Compile A+B as composite → validate composite output +- Step 2: Compile C non-composite with --ref A B --opt-cross-module A → validate C's output +- New source: needs a consumer assembly referencing composite-compiled libs + +### Tier 2: Important (depth coverage) + +**15. CompositeAsyncDevirtualize** — Composite + runtime-async + devirtualization across modules +- Config: `--composite`, `runtime-async=on` +- Validates: AsyncVariant for devirtualized calls +- New source: AsyncInterfaceLib.cs + CompositeAsyncDevirtMain.cs + +**16. CompositeTransitive** — Composite with 3 assemblies in A→B→C chain +- Config: `--composite` +- Validates: ManifestRef×3, CrossModuleInlineInfo for transitive inlining +- Reuse existing source: ExternalLib.cs + InlineableLibTransitive.cs + TransitiveReferences.cs + +**17. AsyncCrossModuleTransitive** — Non-composite, runtime-async + transitive cross-module +- Config: `--opt-cross-module`, `runtime-async=on` +- Validates: ManifestRef×2, AsyncVariant, CrossModuleInlinedMethod +- New source: AsyncExternalLib.cs + AsyncTransitiveLib.cs + AsyncTransitiveMain.cs + +### Tier 3: Extended coverage + +**18. CompositeAsyncTransitive** — Composite + async + transitive (3 assemblies) +- Config: `--composite`, `runtime-async=on` +- Validates: ManifestRef×3, AsyncVariant, CrossModuleInlineInfo +- Can reuse Tier 2 async transitive source with composite config + +**19. MultiStepCompositeAndNonCompositeAsync** — Multi-step with runtime-async +- Step 1: Compile AsyncLib+AsyncMain as composite +- Step 2: Compile AsyncConsumer non-composite with --opt-cross-module, runtime-async=on +- Validates: Both outputs have correct async variants and cross-module references + +## Notes + +- In composite mode, `--opt-cross-module` is NOT used — cross-module inlining is implicit +- Composite emits CrossModuleInlineInfo with `CrossModuleInliningForCrossModuleDataOnly` (cross-module entries only); non-composite emits `CrossModuleAllMethods` (all entries) +- InliningInfo2 is composite-only, per-module, same-module inlining +- For multi-step tests, crossgen2 reads IL metadata from --ref assemblies, not R2R output +- All tests validate R2R metadata only — they don't execute the compiled code diff --git a/r2r-tests-full.diff b/r2r-tests-full.diff new file mode 100644 index 00000000000000..f866aac6343571 --- /dev/null +++ b/r2r-tests-full.diff @@ -0,0 +1,1987 @@ +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs +new file mode 100644 +index 00000000000..8fe039762c1 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs +@@ -0,0 +1,123 @@ ++// Licensed to the .NET Foundation under one or more agreements. ++// The .NET Foundation licenses this file to you under the MIT license. ++ ++using System; ++using System.Diagnostics; ++ ++namespace ILCompiler.ReadyToRun.Tests.Expectations; ++ ++/// ++/// Marks a method as expected to be cross-module inlined into the main R2R image. ++/// The R2R result checker will verify a CHECK_IL_BODY fixup exists for this method's callee. ++/// ++[Conditional("R2R_EXPECTATIONS")] ++[AttributeUsage(AttributeTargets.Method)] ++public sealed class ExpectInlinedAttribute : Attribute ++{ ++ /// ++ /// The fully qualified name of the method expected to be inlined. ++ /// If null, infers from the method body (looks for the first cross-module call). ++ /// ++ public string? MethodName { get; set; } ++} ++ ++/// ++/// Marks a method as expected to have a specific number of RuntimeFunctions in the R2R image. ++/// ++[Conditional("R2R_EXPECTATIONS")] ++[AttributeUsage(AttributeTargets.Method)] ++public sealed class ExpectRuntimeFunctionCountAttribute : Attribute ++{ ++ public int ExpectedCount { get; set; } ++} ++ ++/// ++/// Declares that the R2R image should contain a manifest reference to the specified assembly. ++/// ++[Conditional("R2R_EXPECTATIONS")] ++[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] ++public sealed class ExpectManifestRefAttribute : Attribute ++{ ++ public string AssemblyName { get; } ++ ++ public ExpectManifestRefAttribute(string assemblyName) ++ { ++ AssemblyName = assemblyName; ++ } ++} ++ ++/// ++/// Specifies a crossgen2 command-line option for the main assembly compilation. ++/// Applied at the assembly level of the test case. ++/// ++[Conditional("R2R_EXPECTATIONS")] ++[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] ++public sealed class Crossgen2OptionAttribute : Attribute ++{ ++ public string Option { get; } ++ ++ public Crossgen2OptionAttribute(string option) ++ { ++ Option = option; ++ } ++} ++ ++/// ++/// Declares a dependency assembly that should be compiled before the main test assembly. ++/// The source files are compiled with Roslyn, then optionally crossgen2'd before the main assembly. ++/// ++[Conditional("R2R_EXPECTATIONS")] ++[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] ++public sealed class SetupCompileBeforeAttribute : Attribute ++{ ++ /// ++ /// The output assembly filename (e.g., "InlineableLib.dll"). ++ /// ++ public string OutputName { get; } ++ ++ /// ++ /// Source file paths relative to the test case's Dependencies/ folder. ++ /// ++ public string[] SourceFiles { get; } ++ ++ /// ++ /// Additional assembly references needed to compile this dependency. ++ /// ++ public string[]? References { get; set; } ++ ++ /// ++ /// If true, this assembly is also crossgen2'd before the main assembly. ++ /// ++ public bool Crossgen { get; set; } ++ ++ /// ++ /// Additional crossgen2 options for this dependency assembly. ++ /// ++ public string[]? CrossgenOptions { get; set; } ++ ++ public SetupCompileBeforeAttribute(string outputName, string[] sourceFiles) ++ { ++ OutputName = outputName; ++ SourceFiles = sourceFiles; ++ } ++} ++ ++/// ++/// Marks a method as expected to have R2R compiled code in the output image. ++/// ++[Conditional("R2R_EXPECTATIONS")] ++[AttributeUsage(AttributeTargets.Method)] ++public sealed class ExpectR2RMethodAttribute : Attribute ++{ ++} ++ ++/// ++/// Marks an assembly-level option to enable composite mode compilation. ++/// When present, all SetupCompileBefore assemblies with Crossgen=true are compiled ++/// together with the main assembly using --composite. ++/// ++[Conditional("R2R_EXPECTATIONS")] ++[AttributeUsage(AttributeTargets.Assembly)] ++public sealed class CompositeModeAttribute : Attribute ++{ ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj +new file mode 100644 +index 00000000000..24c1d7f9a74 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj +@@ -0,0 +1,56 @@ ++ ++ ++ ++ ILCompiler.ReadyToRun.Tests ++ $(NetCoreAppToolCurrent) ++ enable ++ false ++ true ++ x64;x86 ++ AnyCPU ++ linux-x64;win-x64;osx-x64 ++ Debug;Release;Checked ++ true ++ -notrait category=failing ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ $(RuntimeBinDir)/crossgen2 ++ ++ ++ $(MicrosoftNetCoreAppRuntimePackRidLibTfmDir) ++ ++ ++ $(MicrosoftNetCoreAppRefPackRefDir) ++ ++ ++ $(CoreCLRArtifactsPath) ++ ++ ++ $(TargetArchitecture) ++ ++ ++ $(TargetOS) ++ ++ ++ $(Configuration) ++ ++ ++ ++ +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs +new file mode 100644 +index 00000000000..b9efb76fab2 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs +@@ -0,0 +1,27 @@ ++// Test: Async method thunks in R2R ++// Validates that runtime-async compiled methods produce the expected ++// RuntimeFunction layout (thunk + async body + resumption stub). ++using System; ++using System.Runtime.CompilerServices; ++using System.Threading.Tasks; ++ ++public static class AsyncMethods ++{ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task TestAsyncInline() ++ { ++ return await AsyncInlineableLib.GetValueAsync(); ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task TestAsyncStringInline() ++ { ++ return await AsyncInlineableLib.GetStringAsync(); ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static int TestSyncFromAsyncLib() ++ { ++ return AsyncInlineableLib.GetValueSync(); ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs +new file mode 100644 +index 00000000000..ba301272868 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs +@@ -0,0 +1,26 @@ ++// Test: Basic cross-module inlining ++// Validates that crossgen2 with --opt-cross-module produces CHECK_IL_BODY fixups ++// for methods inlined from InlineableLib into this main assembly. ++using System; ++using System.Runtime.CompilerServices; ++ ++public static class BasicInlining ++{ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static int TestGetValue() ++ { ++ return InlineableLib.GetValue(); ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static string TestGetString() ++ { ++ return InlineableLib.GetString(); ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static int TestAdd() ++ { ++ return InlineableLib.Add(10, 32); ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs +new file mode 100644 +index 00000000000..0ba24d55635 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs +@@ -0,0 +1,20 @@ ++// Test: Composite mode basic compilation ++// Validates that composite mode R2R compilation with multiple assemblies ++// produces correct manifest references and component assembly entries. ++using System; ++using System.Runtime.CompilerServices; ++ ++public static class CompositeBasic ++{ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static int TestCompositeCall() ++ { ++ return CompositeLib.GetCompositeValue(); ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static object TestCompositeTypeCreation() ++ { ++ return new CompositeLib.CompositeType(); ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs +new file mode 100644 +index 00000000000..153804e6279 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs +@@ -0,0 +1,15 @@ ++using System; ++using System.Runtime.CompilerServices; ++using System.Threading.Tasks; ++ ++public static class AsyncInlineableLib ++{ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static async Task GetValueAsync() => 42; ++ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static async Task GetStringAsync() => "Hello from async"; ++ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static int GetValueSync() => 42; ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs +new file mode 100644 +index 00000000000..2dc5db2de38 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs +@@ -0,0 +1,11 @@ ++using System; ++ ++public static class CompositeLib ++{ ++ public static int GetCompositeValue() => 100; ++ ++ public class CompositeType ++ { ++ public string Name { get; set; } = "Composite"; ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs +new file mode 100644 +index 00000000000..d56f2880564 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs +@@ -0,0 +1,19 @@ ++using System; ++ ++public static class ExternalLib ++{ ++ public static int ExternalValue => 99; ++ ++ public class ExternalType ++ { ++ public int Value { get; set; } ++ } ++ ++ public class Outer ++ { ++ public class Inner ++ { ++ public static int NestedValue => 77; ++ } ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs +new file mode 100644 +index 00000000000..a799cfed728 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs +@@ -0,0 +1,14 @@ ++using System; ++using System.Runtime.CompilerServices; ++ ++public static class InlineableLib ++{ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static int GetValue() => 42; ++ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static string GetString() => "Hello from InlineableLib"; ++ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static int Add(int a, int b) => a + b; ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs +new file mode 100644 +index 00000000000..15fd29dda19 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs +@@ -0,0 +1,14 @@ ++using System; ++using System.Runtime.CompilerServices; ++ ++public static class InlineableLibTransitive ++{ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static int GetExternalValue() => ExternalLib.ExternalValue; ++ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static int GetNestedValue() => ExternalLib.Outer.Inner.NestedValue; ++ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static ExternalLib.ExternalType CreateExternal() => new ExternalLib.ExternalType { Value = 42 }; ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs +new file mode 100644 +index 00000000000..25bac820fc3 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs +@@ -0,0 +1,26 @@ ++// Test: Transitive cross-module references ++// Validates that when InlineableLibTransitive is inlined, its references to ExternalLib ++// are properly encoded in the R2R image (requiring tokens for both libraries). ++using System; ++using System.Runtime.CompilerServices; ++ ++public static class TransitiveReferences ++{ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static int TestTransitiveValue() ++ { ++ return InlineableLibTransitive.GetExternalValue(); ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static int TestNestedTypeAccess() ++ { ++ return InlineableLibTransitive.GetNestedValue(); ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static object TestTransitiveTypeCreation() ++ { ++ return InlineableLibTransitive.CreateExternal(); ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +new file mode 100644 +index 00000000000..620c7da3dc1 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +@@ -0,0 +1,286 @@ ++// Licensed to the .NET Foundation under one or more agreements. ++// The .NET Foundation licenses this file to you under the MIT license. ++ ++using System.Collections.Generic; ++using ILCompiler.ReadyToRun.Tests.TestCasesRunner; ++using Internal.ReadyToRunConstants; ++using Xunit; ++ ++namespace ILCompiler.ReadyToRun.Tests.TestCases; ++ ++/// ++/// xUnit test suites for R2R cross-module resolution tests. ++/// Each test method builds assemblies with Roslyn, crossgen2's them, and validates the R2R output. ++/// ++public class R2RTestSuites ++{ ++ private static readonly KeyValuePair RuntimeAsyncFeature = new("runtime-async", "on"); ++ ++ [Fact] ++ public void BasicCrossModuleInlining() ++ { ++ var expectations = new R2RExpectations(); ++ expectations.ExpectedManifestRefs.Add("InlineableLib"); ++ expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetValue")); ++ expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetString")); ++ expectations.Crossgen2Options.Add("--opt-cross-module:InlineableLib"); ++ ++ var testCase = new R2RTestCase ++ { ++ Name = "BasicCrossModuleInlining", ++ MainSourceResourceName = "CrossModuleInlining/BasicInlining.cs", ++ Dependencies = new List ++ { ++ new DependencyInfo ++ { ++ AssemblyName = "InlineableLib", ++ SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/InlineableLib.cs" }, ++ Crossgen = true, ++ } ++ }, ++ Expectations = expectations, ++ }; ++ ++ new R2RTestRunner().Run(testCase); ++ } ++ ++ [Fact] ++ public void TransitiveReferences() ++ { ++ var expectations = new R2RExpectations(); ++ expectations.ExpectedManifestRefs.Add("InlineableLibTransitive"); ++ expectations.ExpectedManifestRefs.Add("ExternalLib"); ++ expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetExternalValue")); ++ expectations.Crossgen2Options.Add("--opt-cross-module:InlineableLibTransitive"); ++ ++ var testCase = new R2RTestCase ++ { ++ Name = "TransitiveReferences", ++ MainSourceResourceName = "CrossModuleInlining/TransitiveReferences.cs", ++ Dependencies = new List ++ { ++ new DependencyInfo ++ { ++ AssemblyName = "ExternalLib", ++ SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/ExternalLib.cs" }, ++ Crossgen = false, ++ }, ++ new DependencyInfo ++ { ++ AssemblyName = "InlineableLibTransitive", ++ SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/InlineableLibTransitive.cs" }, ++ Crossgen = true, ++ AdditionalReferences = { "ExternalLib" }, ++ } ++ }, ++ Expectations = expectations, ++ }; ++ ++ new R2RTestRunner().Run(testCase); ++ } ++ ++ [Fact] ++ public void AsyncCrossModuleInlining() ++ { ++ var expectations = new R2RExpectations(); ++ expectations.ExpectedManifestRefs.Add("AsyncInlineableLib"); ++ expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetValueAsync")); ++ expectations.Crossgen2Options.Add("--opt-cross-module:AsyncInlineableLib"); ++ ++ var testCase = new R2RTestCase ++ { ++ Name = "AsyncCrossModuleInlining", ++ MainSourceResourceName = "CrossModuleInlining/AsyncMethods.cs", ++ Dependencies = new List ++ { ++ new DependencyInfo ++ { ++ AssemblyName = "AsyncInlineableLib", ++ SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/AsyncInlineableLib.cs" }, ++ Crossgen = true, ++ } ++ }, ++ Expectations = expectations, ++ }; ++ ++ new R2RTestRunner().Run(testCase); ++ } ++ ++ [Fact] ++ public void CompositeBasic() ++ { ++ var expectations = new R2RExpectations ++ { ++ CompositeMode = true, ++ }; ++ expectations.ExpectedManifestRefs.Add("CompositeLib"); ++ ++ var testCase = new R2RTestCase ++ { ++ Name = "CompositeBasic", ++ MainSourceResourceName = "CrossModuleInlining/CompositeBasic.cs", ++ Dependencies = new List ++ { ++ new DependencyInfo ++ { ++ AssemblyName = "CompositeLib", ++ SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/CompositeLib.cs" }, ++ Crossgen = true, ++ } ++ }, ++ Expectations = expectations, ++ }; ++ ++ new R2RTestRunner().Run(testCase); ++ } ++ ++ /// ++ /// PR #124203: Async methods produce [ASYNC] variant entries with resumption stubs. ++ /// PR #121456: Resumption stubs are emitted as ResumptionStubEntryPoint fixups. ++ /// PR #123643: Methods with GC refs across awaits produce ContinuationLayout fixups. ++ /// ++ [Fact] ++ public void RuntimeAsyncMethodEmission() ++ { ++ string attrSource = R2RTestCaseCompiler.ReadEmbeddedSource( ++ "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"); ++ string mainSource = R2RTestCaseCompiler.ReadEmbeddedSource( ++ "RuntimeAsync/BasicAsyncEmission.cs"); ++ ++ var expectations = new R2RExpectations(); ++ expectations.Features.Add(RuntimeAsyncFeature); ++ expectations.ExpectedAsyncVariantMethods.Add("SimpleAsyncMethod"); ++ expectations.ExpectedAsyncVariantMethods.Add("AsyncVoidReturn"); ++ expectations.ExpectedAsyncVariantMethods.Add("ValueTaskMethod"); ++ ++ var testCase = new R2RTestCase ++ { ++ Name = "RuntimeAsyncMethodEmission", ++ MainSourceResourceName = "RuntimeAsync/BasicAsyncEmission.cs", ++ MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, ++ Dependencies = new List(), ++ Expectations = expectations, ++ }; ++ ++ new R2RTestRunner().Run(testCase); ++ } ++ ++ /// ++ /// PR #123643: Async methods capturing GC refs across await points ++ /// produce ContinuationLayout fixups encoding the GC ref map. ++ /// PR #124203: Resumption stubs for methods with suspension points. ++ /// ++ [Fact] ++ public void RuntimeAsyncContinuationLayout() ++ { ++ string attrSource = R2RTestCaseCompiler.ReadEmbeddedSource( ++ "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"); ++ string mainSource = R2RTestCaseCompiler.ReadEmbeddedSource( ++ "RuntimeAsync/AsyncWithContinuation.cs"); ++ ++ var expectations = new R2RExpectations ++ { ++ ExpectContinuationLayout = true, ++ ExpectResumptionStubFixup = true, ++ }; ++ expectations.Features.Add(RuntimeAsyncFeature); ++ expectations.ExpectedAsyncVariantMethods.Add("CaptureObjectAcrossAwait"); ++ expectations.ExpectedAsyncVariantMethods.Add("CaptureMultipleRefsAcrossAwait"); ++ ++ var testCase = new R2RTestCase ++ { ++ Name = "RuntimeAsyncContinuationLayout", ++ MainSourceResourceName = "RuntimeAsync/AsyncWithContinuation.cs", ++ MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, ++ Dependencies = new List(), ++ Expectations = expectations, ++ }; ++ ++ new R2RTestRunner().Run(testCase); ++ } ++ ++ /// ++ /// PR #125420: Devirtualization of async methods through ++ /// AsyncAwareVirtualMethodResolutionAlgorithm. ++ /// ++ [Fact] ++ public void RuntimeAsyncDevirtualize() ++ { ++ var expectations = new R2RExpectations(); ++ expectations.Features.Add(RuntimeAsyncFeature); ++ expectations.ExpectedAsyncVariantMethods.Add("GetValueAsync"); ++ ++ var testCase = new R2RTestCase ++ { ++ Name = "RuntimeAsyncDevirtualize", ++ MainSourceResourceName = "RuntimeAsync/AsyncDevirtualize.cs", ++ MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, ++ Dependencies = new List(), ++ Expectations = expectations, ++ }; ++ ++ new R2RTestRunner().Run(testCase); ++ } ++ ++ /// ++ /// PR #124203: Async methods without yield points may omit resumption stubs. ++ /// Validates that no-yield async methods still produce [ASYNC] variants. ++ /// ++ [Fact] ++ public void RuntimeAsyncNoYield() ++ { ++ var expectations = new R2RExpectations(); ++ expectations.Features.Add(RuntimeAsyncFeature); ++ expectations.ExpectedAsyncVariantMethods.Add("AsyncButNoAwait"); ++ expectations.ExpectedAsyncVariantMethods.Add("AsyncWithConditionalAwait"); ++ ++ var testCase = new R2RTestCase ++ { ++ Name = "RuntimeAsyncNoYield", ++ MainSourceResourceName = "RuntimeAsync/AsyncNoYield.cs", ++ MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, ++ Dependencies = new List(), ++ Expectations = expectations, ++ }; ++ ++ new R2RTestRunner().Run(testCase); ++ } ++ ++ /// ++ /// PR #121679: MutableModule async references + cross-module inlining ++ /// of runtime-async methods with cross-module dependency. ++ /// ++ [Fact] ++ public void RuntimeAsyncCrossModule() ++ { ++ var expectations = new R2RExpectations(); ++ expectations.Features.Add(RuntimeAsyncFeature); ++ expectations.ExpectedManifestRefs.Add("AsyncDepLib"); ++ expectations.ExpectedAsyncVariantMethods.Add("CallCrossModuleAsync"); ++ expectations.Crossgen2Options.Add("--opt-cross-module:AsyncDepLib"); ++ ++ var testCase = new R2RTestCase ++ { ++ Name = "RuntimeAsyncCrossModule", ++ MainSourceResourceName = "RuntimeAsync/AsyncCrossModule.cs", ++ MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, ++ Dependencies = new List ++ { ++ new DependencyInfo ++ { ++ AssemblyName = "AsyncDepLib", ++ SourceResourceNames = new[] ++ { ++ "RuntimeAsync/Dependencies/AsyncDepLib.cs", ++ "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" ++ }, ++ Crossgen = true, ++ Features = { RuntimeAsyncFeature }, ++ } ++ }, ++ Expectations = expectations, ++ }; ++ ++ new R2RTestRunner().Run(testCase); ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs +new file mode 100644 +index 00000000000..28534b5a9fc +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs +@@ -0,0 +1,28 @@ ++// Test: Cross-module async method inlining ++// Validates that async methods from a dependency library can be ++// cross-module inlined, creating manifest refs and CHECK_IL_BODY fixups. ++using System; ++using System.Runtime.CompilerServices; ++using System.Threading.Tasks; ++ ++public static class AsyncCrossModule ++{ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task CallCrossModuleAsync() ++ { ++ return await AsyncDepLib.GetValueAsync(); ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task CallCrossModuleStringAsync() ++ { ++ return await AsyncDepLib.GetStringAsync(); ++ } ++ ++ // Call a non-async sync method from async lib ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static int CallCrossModuleSync() ++ { ++ return AsyncDepLib.GetValueSync(); ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs +new file mode 100644 +index 00000000000..76d69eecf2f +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs +@@ -0,0 +1,56 @@ ++// Test: Async virtual method devirtualization in R2R ++// Validates that sealed class and interface dispatch of async methods ++// produces devirtualized direct call entries in the R2R image. ++using System; ++using System.Runtime.CompilerServices; ++using System.Threading.Tasks; ++ ++public interface IAsyncService ++{ ++ Task GetValueAsync(); ++} ++ ++public class OpenImpl : IAsyncService ++{ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public virtual async Task GetValueAsync() ++ { ++ await Task.Yield(); ++ return 10; ++ } ++} ++ ++public sealed class SealedImpl : IAsyncService ++{ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public async Task GetValueAsync() ++ { ++ await Task.Yield(); ++ return 20; ++ } ++} ++ ++public static class AsyncDevirtualize ++{ ++ // Sealed type known at compile time — should devirtualize ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task CallOnSealed(SealedImpl obj) ++ { ++ return await obj.GetValueAsync(); ++ } ++ ++ // newobj gives exact type info — should devirtualize through resolveVirtualMethod ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task CallOnNewOpen() ++ { ++ IAsyncService svc = new OpenImpl(); ++ return await svc.GetValueAsync(); ++ } ++ ++ // Generic constrained dispatch ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task CallGenericConstrained(T obj) where T : IAsyncService ++ { ++ return await obj.GetValueAsync(); ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs +new file mode 100644 +index 00000000000..28ddb079495 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs +@@ -0,0 +1,23 @@ ++// Test: Async method without yields (no suspension point) ++// When a runtime-async method never actually awaits, crossgen2 may ++// omit the resumption stub. This tests that edge case. ++using System; ++using System.Runtime.CompilerServices; ++using System.Threading.Tasks; ++ ++public static class AsyncNoYield ++{ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task AsyncButNoAwait() ++ { ++ return 42; ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task AsyncWithConditionalAwait(bool doAwait) ++ { ++ if (doAwait) ++ await Task.Yield(); ++ return 1; ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs +new file mode 100644 +index 00000000000..29a8fc879ec +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs +@@ -0,0 +1,26 @@ ++// Test: Async method that captures GC refs across await ++// This forces the compiler to emit a ContinuationLayout fixup. ++using System; ++using System.Runtime.CompilerServices; ++using System.Threading.Tasks; ++ ++public static class AsyncWithContinuation ++{ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task CaptureObjectAcrossAwait() ++ { ++ object o = new object(); ++ string s = "hello"; ++ await Task.Yield(); ++ return s + o.GetHashCode(); ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task CaptureMultipleRefsAcrossAwait() ++ { ++ int[] arr = new int[] { 1, 2, 3 }; ++ string text = "world"; ++ await Task.Yield(); ++ return arr[0] + text.Length; ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs +new file mode 100644 +index 00000000000..c6137cfc309 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs +@@ -0,0 +1,36 @@ ++// Test: Basic async method emission in R2R ++// Validates that runtime-async methods produce [ASYNC] variant entries and ++// resumption stubs in the R2R image. ++using System; ++using System.Runtime.CompilerServices; ++using System.Threading.Tasks; ++ ++public static class BasicAsyncEmission ++{ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task SimpleAsyncMethod() ++ { ++ await Task.Yield(); ++ return 42; ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async Task AsyncVoidReturn() ++ { ++ await Task.Yield(); ++ } ++ ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static async ValueTask ValueTaskMethod() ++ { ++ await Task.Yield(); ++ return "hello"; ++ } ++ ++ // Non-async method that returns Task (no await) — should NOT get async variant ++ [MethodImpl(MethodImplOptions.NoInlining)] ++ public static Task SyncTaskReturning() ++ { ++ return Task.FromResult(1); ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs +new file mode 100644 +index 00000000000..bb10453f70b +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs +@@ -0,0 +1,28 @@ ++// Dependency library for async cross-module tests. ++// Contains runtime-async methods that should be inlineable. ++using System; ++using System.Runtime.CompilerServices; ++using System.Threading.Tasks; ++ ++public static class AsyncDepLib ++{ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static async Task GetValueAsync() ++ { ++ await Task.Yield(); ++ return 42; ++ } ++ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static async Task GetStringAsync() ++ { ++ await Task.Yield(); ++ return "async_hello"; ++ } ++ ++ [MethodImpl(MethodImplOptions.AggressiveInlining)] ++ public static int GetValueSync() ++ { ++ return 99; ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs +new file mode 100644 +index 00000000000..e481c3bcefd +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs +@@ -0,0 +1,10 @@ ++// Licensed to the .NET Foundation under one or more agreements. ++// The .NET Foundation licenses this file to you under the MIT license. ++ ++namespace System.Runtime.CompilerServices; ++ ++[AttributeUsage(AttributeTargets.Method)] ++internal class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribute ++{ ++ public bool RuntimeAsync { get; } = runtimeAsync; ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs +new file mode 100644 +index 00000000000..6aa4f9d3479 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs +@@ -0,0 +1,162 @@ ++// Licensed to the .NET Foundation under one or more agreements. ++// The .NET Foundation licenses this file to you under the MIT license. ++ ++using System; ++using System.Collections.Generic; ++using System.Diagnostics; ++using System.IO; ++using System.Linq; ++using System.Text; ++ ++namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; ++ ++/// ++/// Result of a crossgen2 compilation step. ++/// ++internal sealed record R2RCompilationResult( ++ string OutputPath, ++ int ExitCode, ++ string StandardOutput, ++ string StandardError) ++{ ++ public bool Success => ExitCode == 0; ++} ++ ++/// ++/// Options for a single crossgen2 compilation step. ++/// ++internal sealed class R2RCompilationOptions ++{ ++ public required string InputPath { get; init; } ++ public required string OutputPath { get; init; } ++ public List ReferencePaths { get; init; } = new(); ++ public List ExtraArgs { get; init; } = new(); ++ public bool Composite { get; init; } ++ public List? CompositeInputPaths { get; init; } ++ public List? InputBubbleRefs { get; init; } ++} ++ ++/// ++/// Invokes crossgen2 out-of-process to produce R2R images. ++/// ++internal sealed class R2RDriver ++{ ++ private readonly string _crossgen2Dir; ++ ++ public R2RDriver() ++ { ++ _crossgen2Dir = TestPaths.Crossgen2Dir; ++ ++ if (!File.Exists(TestPaths.Crossgen2Dll)) ++ throw new FileNotFoundException($"crossgen2.dll not found at {TestPaths.Crossgen2Dll}"); ++ } ++ ++ /// ++ /// Runs crossgen2 on a single assembly. ++ /// ++ public R2RCompilationResult Compile(R2RCompilationOptions options) ++ { ++ var args = new List(); ++ ++ if (options.Composite) ++ { ++ args.Add("--composite"); ++ if (options.CompositeInputPaths is not null) ++ { ++ foreach (string input in options.CompositeInputPaths) ++ args.Add(input); ++ } ++ } ++ else ++ { ++ args.Add(options.InputPath); ++ } ++ ++ args.Add("-o"); ++ args.Add(options.OutputPath); ++ ++ foreach (string refPath in options.ReferencePaths) ++ { ++ args.Add("-r"); ++ args.Add(refPath); ++ } ++ ++ if (options.InputBubbleRefs is not null) ++ { ++ foreach (string bubbleRef in options.InputBubbleRefs) ++ { ++ args.Add("--inputbubbleref"); ++ args.Add(bubbleRef); ++ } ++ } ++ ++ args.AddRange(options.ExtraArgs); ++ ++ return RunCrossgen2(args); ++ } ++ ++ /// ++ /// Crossgen2 a dependency assembly (simple single-assembly R2R). ++ /// ++ public R2RCompilationResult CompileDependency(string inputPath, string outputPath, IEnumerable referencePaths) ++ { ++ return Compile(new R2RCompilationOptions ++ { ++ InputPath = inputPath, ++ OutputPath = outputPath, ++ ReferencePaths = referencePaths.ToList() ++ }); ++ } ++ ++ private R2RCompilationResult RunCrossgen2(List crossgen2Args) ++ { ++ // Use dotnet exec to invoke crossgen2.dll ++ string dotnetHost = TestPaths.DotNetHost; ++ string crossgen2Dll = TestPaths.Crossgen2Dll; ++ ++ var allArgs = new List { "exec", crossgen2Dll }; ++ allArgs.AddRange(crossgen2Args); ++ ++ string argsString = string.Join(" ", allArgs.Select(QuoteIfNeeded)); ++ ++ var psi = new ProcessStartInfo ++ { ++ FileName = dotnetHost, ++ Arguments = argsString, ++ RedirectStandardOutput = true, ++ RedirectStandardError = true, ++ UseShellExecute = false, ++ CreateNoWindow = true, ++ }; ++ ++ // Strip environment variables that interfere with crossgen2 ++ string[] envVarsToStrip = { "DOTNET_GCName", "DOTNET_GCStress", "DOTNET_HeapVerify", "DOTNET_ReadyToRun" }; ++ foreach (string envVar in envVarsToStrip) ++ { ++ psi.Environment[envVar] = null; ++ } ++ ++ using var process = Process.Start(psi)!; ++ string stdout = process.StandardOutput.ReadToEnd(); ++ string stderr = process.StandardError.ReadToEnd(); ++ process.WaitForExit(); ++ ++ string outputPath = crossgen2Args ++ .SkipWhile(a => a != "-o") ++ .Skip(1) ++ .FirstOrDefault() ?? "unknown"; ++ ++ return new R2RCompilationResult( ++ outputPath, ++ process.ExitCode, ++ stdout, ++ stderr); ++ } ++ ++ private static string QuoteIfNeeded(string arg) ++ { ++ if (arg.Contains(' ') || arg.Contains('"')) ++ return $"\"{arg.Replace("\"", "\\\"")}\""; ++ return arg; ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +new file mode 100644 +index 00000000000..e0565ad1f07 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +@@ -0,0 +1,318 @@ ++// Licensed to the .NET Foundation under one or more agreements. ++// The .NET Foundation licenses this file to you under the MIT license. ++ ++using System; ++using System.Collections.Generic; ++using System.IO; ++using System.Linq; ++using System.Reflection.Metadata; ++using System.Reflection.PortableExecutable; ++using ILCompiler.Reflection.ReadyToRun; ++using Internal.ReadyToRunConstants; ++using Xunit; ++ ++namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; ++ ++/// ++/// Parsed expectations from a test case's assembly-level and method-level attributes. ++/// ++internal sealed class R2RExpectations ++{ ++ public List ExpectedManifestRefs { get; } = new(); ++ public List ExpectedInlinedMethods { get; } = new(); ++ public bool CompositeMode { get; set; } ++ public List Crossgen2Options { get; } = new(); ++ /// ++ /// Roslyn feature flags for the main assembly (e.g. runtime-async=on). ++ /// ++ public List> Features { get; } = new(); ++ /// ++ /// Method names expected to have [ASYNC] variant entries in the R2R image. ++ /// ++ public List ExpectedAsyncVariantMethods { get; } = new(); ++ /// ++ /// Method names expected to have [RESUME] (resumption stub) entries. ++ /// ++ public List ExpectedResumptionStubs { get; } = new(); ++ /// ++ /// If true, expect at least one ContinuationLayout fixup in the image. ++ /// ++ public bool ExpectContinuationLayout { get; set; } ++ /// ++ /// If true, expect at least one ResumptionStubEntryPoint fixup in the image. ++ /// ++ public bool ExpectResumptionStubFixup { get; set; } ++ /// ++ /// Fixup kinds that must be present somewhere in the image. ++ /// ++ public List ExpectedFixupKinds { get; } = new(); ++} ++ ++internal sealed record ExpectedInlinedMethod(string MethodName); ++ ++/// ++/// Validates R2R images against test expectations using ReadyToRunReader. ++/// ++internal sealed class R2RResultChecker ++{ ++ /// ++ /// Validates the main R2R image against expectations. ++ /// ++ public void Check(string r2rImagePath, R2RExpectations expectations) ++ { ++ Assert.True(File.Exists(r2rImagePath), $"R2R image not found: {r2rImagePath}"); ++ ++ using var fileStream = File.OpenRead(r2rImagePath); ++ using var peReader = new PEReader(fileStream); ++ ++ Assert.True(ReadyToRunReader.IsReadyToRunImage(peReader), ++ $"'{Path.GetFileName(r2rImagePath)}' is not a valid R2R image"); ++ ++ var reader = new ReadyToRunReader(new SimpleAssemblyResolver(), r2rImagePath); ++ ++ CheckManifestRefs(reader, expectations, r2rImagePath); ++ CheckInlinedMethods(reader, expectations, r2rImagePath); ++ CheckAsyncVariantMethods(reader, expectations, r2rImagePath); ++ CheckResumptionStubs(reader, expectations, r2rImagePath); ++ CheckFixupKinds(reader, expectations, r2rImagePath); ++ } ++ ++ private static void CheckManifestRefs(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) ++ { ++ if (expectations.ExpectedManifestRefs.Count == 0) ++ return; ++ ++ // Get all assembly references (both MSIL and manifest) ++ var allRefs = new HashSet(StringComparer.OrdinalIgnoreCase); ++ ++ // Read MSIL AssemblyRef table ++ var globalMetadata = reader.GetGlobalMetadata(); ++ var mdReader = globalMetadata.MetadataReader; ++ foreach (var handle in mdReader.AssemblyReferences) ++ { ++ var assemblyRef = mdReader.GetAssemblyReference(handle); ++ string name = mdReader.GetString(assemblyRef.Name); ++ allRefs.Add(name); ++ } ++ ++ // Read manifest references (extra refs beyond MSIL table) ++ foreach (var kvp in reader.ManifestReferenceAssemblies) ++ { ++ allRefs.Add(kvp.Key); ++ } ++ ++ foreach (string expected in expectations.ExpectedManifestRefs) ++ { ++ Assert.True(allRefs.Contains(expected), ++ $"Expected assembly reference '{expected}' not found in R2R image '{Path.GetFileName(imagePath)}'. " + ++ $"Found: [{string.Join(", ", allRefs.OrderBy(s => s))}]"); ++ } ++ } ++ ++ private static void CheckInlinedMethods(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) ++ { ++ if (expectations.ExpectedInlinedMethods.Count == 0) ++ return; ++ ++ var checkIlBodySignatures = new HashSet(StringComparer.OrdinalIgnoreCase); ++ var formattingOptions = new SignatureFormattingOptions(); ++ var allFixupSummary = new List(); ++ ++ void CollectFixups(ReadyToRunMethod method) ++ { ++ foreach (var cell in method.Fixups) ++ { ++ if (cell.Signature is null) ++ continue; ++ ++ string sigText = cell.Signature.ToString(formattingOptions); ++ allFixupSummary.Add($"[{cell.Signature.FixupKind}] {sigText}"); ++ ++ if (cell.Signature.FixupKind is ReadyToRunFixupKind.Check_IL_Body or ReadyToRunFixupKind.Verify_IL_Body) ++ { ++ checkIlBodySignatures.Add(sigText); ++ } ++ } ++ } ++ ++ foreach (var assembly in reader.ReadyToRunAssemblies) ++ { ++ foreach (var method in assembly.Methods) ++ CollectFixups(method); ++ } ++ ++ foreach (var instanceMethod in reader.InstanceMethods) ++ CollectFixups(instanceMethod.Method); ++ ++ foreach (var expected in expectations.ExpectedInlinedMethods) ++ { ++ bool found = checkIlBodySignatures.Any(f => ++ f.Contains(expected.MethodName, StringComparison.OrdinalIgnoreCase)); ++ ++ Assert.True(found, ++ $"Expected CHECK_IL_BODY fixup for '{expected.MethodName}' not found in '{Path.GetFileName(imagePath)}'. " + ++ $"CHECK_IL_BODY fixups: [{string.Join(", ", checkIlBodySignatures)}]. " + ++ $"All fixups: [{string.Join("; ", allFixupSummary)}]"); ++ } ++ } ++ ++ private static List GetAllMethods(ReadyToRunReader reader) ++ { ++ var methods = new List(); ++ foreach (var assembly in reader.ReadyToRunAssemblies) ++ methods.AddRange(assembly.Methods); ++ foreach (var instanceMethod in reader.InstanceMethods) ++ methods.Add(instanceMethod.Method); ++ ++ return methods; ++ } ++ ++ private static void CheckAsyncVariantMethods(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) ++ { ++ if (expectations.ExpectedAsyncVariantMethods.Count == 0) ++ return; ++ ++ var allMethods = GetAllMethods(reader); ++ var asyncMethods = allMethods ++ .Where(m => m.SignatureString.Contains("[ASYNC]", StringComparison.OrdinalIgnoreCase)) ++ .Select(m => m.SignatureString) ++ .ToList(); ++ ++ foreach (string expected in expectations.ExpectedAsyncVariantMethods) ++ { ++ bool found = asyncMethods.Any(sig => ++ sig.Contains(expected, StringComparison.OrdinalIgnoreCase)); ++ ++ Assert.True(found, ++ $"Expected [ASYNC] variant for '{expected}' not found in '{Path.GetFileName(imagePath)}'. " + ++ $"Async methods: [{string.Join(", ", asyncMethods)}]. " + ++ $"All methods: [{string.Join(", ", allMethods.Select(m => m.SignatureString).Take(30))}]"); ++ } ++ } ++ ++ private static void CheckResumptionStubs(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) ++ { ++ if (expectations.ExpectedResumptionStubs.Count == 0 && !expectations.ExpectResumptionStubFixup) ++ return; ++ ++ var allMethods = GetAllMethods(reader); ++ var resumeMethods = allMethods ++ .Where(m => m.SignatureString.Contains("[RESUME]", StringComparison.OrdinalIgnoreCase)) ++ .Select(m => m.SignatureString) ++ .ToList(); ++ ++ foreach (string expected in expectations.ExpectedResumptionStubs) ++ { ++ bool found = resumeMethods.Any(sig => ++ sig.Contains(expected, StringComparison.OrdinalIgnoreCase)); ++ ++ Assert.True(found, ++ $"Expected [RESUME] stub for '{expected}' not found in '{Path.GetFileName(imagePath)}'. " + ++ $"Resume methods: [{string.Join(", ", resumeMethods)}]. " + ++ $"All methods: [{string.Join(", ", allMethods.Select(m => m.SignatureString).Take(30))}]"); ++ } ++ ++ if (expectations.ExpectResumptionStubFixup) ++ { ++ var formattingOptions = new SignatureFormattingOptions(); ++ bool hasResumptionFixup = allMethods.Any(m => ++ m.Fixups.Any(c => ++ c.Signature?.FixupKind == ReadyToRunFixupKind.ResumptionStubEntryPoint)); ++ ++ Assert.True(hasResumptionFixup, ++ $"Expected ResumptionStubEntryPoint fixup not found in '{Path.GetFileName(imagePath)}'."); ++ } ++ } ++ ++ private static void CheckFixupKinds(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) ++ { ++ if (expectations.ExpectedFixupKinds.Count == 0 && !expectations.ExpectContinuationLayout) ++ return; ++ ++ var allMethods = GetAllMethods(reader); ++ var presentKinds = new HashSet(); ++ foreach (var method in allMethods) ++ { ++ if (method.Fixups is null) ++ continue; ++ foreach (var cell in method.Fixups) ++ { ++ if (cell.Signature is not null) ++ presentKinds.Add(cell.Signature.FixupKind); ++ } ++ } ++ ++ if (expectations.ExpectContinuationLayout) ++ { ++ Assert.True(presentKinds.Contains(ReadyToRunFixupKind.ContinuationLayout), ++ $"Expected ContinuationLayout fixup not found in '{Path.GetFileName(imagePath)}'. " + ++ $"Present fixup kinds: [{string.Join(", ", presentKinds)}]"); ++ } ++ ++ foreach (var expectedKind in expectations.ExpectedFixupKinds) ++ { ++ Assert.True(presentKinds.Contains(expectedKind), ++ $"Expected fixup kind '{expectedKind}' not found in '{Path.GetFileName(imagePath)}'. " + ++ $"Present fixup kinds: [{string.Join(", ", presentKinds)}]"); ++ } ++ } ++} ++ ++/// ++/// Simple assembly resolver that looks in the same directory as the input image. ++/// ++internal sealed class SimpleAssemblyResolver : IAssemblyResolver ++{ ++ private readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase); ++ ++ public IAssemblyMetadata? FindAssembly(MetadataReader metadataReader, AssemblyReferenceHandle assemblyReferenceHandle, string parentFile) ++ { ++ var assemblyRef = metadataReader.GetAssemblyReference(assemblyReferenceHandle); ++ string name = metadataReader.GetString(assemblyRef.Name); ++ return FindAssembly(name, parentFile); ++ } ++ ++ public IAssemblyMetadata? FindAssembly(string simpleName, string parentFile) ++ { ++ string? dir = Path.GetDirectoryName(parentFile); ++ if (dir is null) ++ return null; ++ ++ string candidate = Path.Combine(dir, simpleName + ".dll"); ++ if (!File.Exists(candidate)) ++ { ++ // Try in runtime pack ++ candidate = Path.Combine(TestPaths.RuntimePackDir, simpleName + ".dll"); ++ } ++ ++ if (!File.Exists(candidate)) ++ return null; ++ ++ return new SimpleAssemblyMetadata(candidate); ++ } ++} ++ ++/// ++/// Simple assembly metadata wrapper. ++/// ++internal sealed class SimpleAssemblyMetadata : IAssemblyMetadata, IDisposable ++{ ++ private readonly FileStream _stream; ++ private readonly PEReader _peReader; ++ ++ public SimpleAssemblyMetadata(string path) ++ { ++ _stream = File.OpenRead(path); ++ _peReader = new PEReader(_stream); ++ } ++ ++ public PEReader ImageReader => _peReader; ++ ++ public MetadataReader MetadataReader => _peReader.GetMetadataReader(); ++ ++ public void Dispose() ++ { ++ _peReader.Dispose(); ++ _stream.Dispose(); ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs +new file mode 100644 +index 00000000000..c92c52d16c5 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs +@@ -0,0 +1,131 @@ ++// Licensed to the .NET Foundation under one or more agreements. ++// The .NET Foundation licenses this file to you under the MIT license. ++ ++using System; ++using System.Collections.Generic; ++using System.IO; ++using System.Linq; ++using System.Reflection; ++using System.Reflection.Metadata; ++using Microsoft.CodeAnalysis; ++using Microsoft.CodeAnalysis.CSharp; ++using Microsoft.CodeAnalysis.Emit; ++ ++namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; ++ ++/// ++/// Compiles C# source code into assemblies using Roslyn at test time. ++/// ++internal sealed class R2RTestCaseCompiler ++{ ++ private readonly string _outputDir; ++ private readonly List _frameworkReferences; ++ ++ public R2RTestCaseCompiler(string outputDir) ++ { ++ _outputDir = outputDir; ++ _frameworkReferences = new List(); ++ ++ // Add reference assemblies from the ref pack (needed for Roslyn compilation) ++ string refPackDir = TestPaths.RefPackDir; ++ if (Directory.Exists(refPackDir)) ++ { ++ foreach (string refPath in Directory.EnumerateFiles(refPackDir, "*.dll")) ++ { ++ _frameworkReferences.Add(MetadataReference.CreateFromFile(refPath)); ++ } ++ } ++ else ++ { ++ // Fallback to runtime pack implementation assemblies ++ foreach (string refPath in TestPaths.GetFrameworkReferencePaths()) ++ { ++ _frameworkReferences.Add(MetadataReference.CreateFromFile(refPath)); ++ } ++ } ++ } ++ ++ /// ++ /// Compiles a single assembly from source files. ++ /// ++ /// Name of the output assembly (without .dll extension). ++ /// C# source code strings. ++ /// Paths to additional assembly references. ++ /// Library or ConsoleApplication. ++ /// Additional preprocessor defines. ++ /// Roslyn feature flags (e.g. "runtime-async=on"). ++ /// Path to the compiled assembly. ++ public string CompileAssembly( ++ string assemblyName, ++ IEnumerable sources, ++ IEnumerable? additionalReferences = null, ++ OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, ++ IEnumerable? additionalDefines = null, ++ IEnumerable>? features = null) ++ { ++ var parseOptions = new CSharpParseOptions( ++ LanguageVersion.Latest, ++ preprocessorSymbols: additionalDefines); ++ ++ if (features is not null) ++ parseOptions = parseOptions.WithFeatures(features); ++ ++ var syntaxTrees = sources.Select(src => ++ CSharpSyntaxTree.ParseText(src, parseOptions)); ++ ++ var references = new List(_frameworkReferences); ++ if (additionalReferences is not null) ++ { ++ foreach (string refPath in additionalReferences) ++ { ++ references.Add(MetadataReference.CreateFromFile(refPath)); ++ } ++ } ++ ++ var compilation = CSharpCompilation.Create( ++ assemblyName, ++ syntaxTrees, ++ references, ++ new CSharpCompilationOptions(outputKind) ++ .WithOptimizationLevel(OptimizationLevel.Release) ++ .WithAllowUnsafe(true) ++ .WithNullableContextOptions(NullableContextOptions.Enable)); ++ ++ string outputPath = Path.Combine(_outputDir, assemblyName + ".dll"); ++ EmitResult result = compilation.Emit(outputPath); ++ ++ if (!result.Success) ++ { ++ var errors = result.Diagnostics ++ .Where(d => d.Severity == DiagnosticSeverity.Error) ++ .Select(d => d.ToString()); ++ throw new InvalidOperationException( ++ $"Compilation of '{assemblyName}' failed:\n{string.Join("\n", errors)}"); ++ } ++ ++ return outputPath; ++ } ++ ++ /// ++ /// Reads an embedded resource from the test assembly. ++ /// ++ public static string ReadEmbeddedSource(string resourceName) ++ { ++ var assembly = Assembly.GetExecutingAssembly(); ++ using Stream? stream = assembly.GetManifestResourceStream(resourceName); ++ if (stream is null) ++ { ++ // Try with different path separator ++ string altName = resourceName.Replace('/', '\\'); ++ using Stream? altStream = assembly.GetManifestResourceStream(altName); ++ if (altStream is null) ++ throw new FileNotFoundException($"Embedded resource not found: '{resourceName}'. Available: {string.Join(", ", assembly.GetManifestResourceNames())}"); ++ ++ using var altReader = new StreamReader(altStream); ++ return altReader.ReadToEnd(); ++ } ++ ++ using var reader = new StreamReader(stream); ++ return reader.ReadToEnd(); ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +new file mode 100644 +index 00000000000..22131fe08ac +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +@@ -0,0 +1,221 @@ ++// Licensed to the .NET Foundation under one or more agreements. ++// The .NET Foundation licenses this file to you under the MIT license. ++ ++using System; ++using System.Collections.Generic; ++using System.IO; ++using System.Linq; ++using ILCompiler.ReadyToRun.Tests.Expectations; ++using Xunit; ++ ++namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; ++ ++/// ++/// Describes a test case: a main source file with its dependencies and expectations. ++/// ++internal sealed class R2RTestCase ++{ ++ public required string Name { get; init; } ++ public required string MainSourceResourceName { get; init; } ++ /// ++ /// Additional source files to compile with the main assembly (e.g. shared attribute files). ++ /// ++ public string[]? MainExtraSourceResourceNames { get; init; } ++ public required List Dependencies { get; init; } ++ public required R2RExpectations Expectations { get; init; } ++} ++ ++/// ++/// Describes a dependency assembly for a test case. ++/// ++internal sealed class DependencyInfo ++{ ++ public required string AssemblyName { get; init; } ++ public required string[] SourceResourceNames { get; init; } ++ public bool Crossgen { get; init; } ++ public List CrossgenOptions { get; init; } = new(); ++ public List AdditionalReferences { get; init; } = new(); ++ /// ++ /// Roslyn feature flags for this dependency (e.g. runtime-async=on). ++ /// ++ public List> Features { get; init; } = new(); ++} ++ ++/// ++/// Orchestrates the full R2R test pipeline: compile → crossgen2 → validate. ++/// ++internal sealed class R2RTestRunner ++{ ++ /// ++ /// Runs a test case end-to-end. ++ /// ++ public void Run(R2RTestCase testCase) ++ { ++ string tempDir = Path.Combine(Path.GetTempPath(), "R2RTests", testCase.Name, Guid.NewGuid().ToString("N")[..8]); ++ string ilDir = Path.Combine(tempDir, "il"); ++ string r2rDir = Path.Combine(tempDir, "r2r"); ++ ++ try ++ { ++ Directory.CreateDirectory(ilDir); ++ Directory.CreateDirectory(r2rDir); ++ ++ // Step 1: Compile all dependencies with Roslyn ++ var compiler = new R2RTestCaseCompiler(ilDir); ++ var compiledDeps = new List<(DependencyInfo Dep, string IlPath)>(); ++ ++ foreach (var dep in testCase.Dependencies) ++ { ++ var sources = dep.SourceResourceNames ++ .Select(R2RTestCaseCompiler.ReadEmbeddedSource) ++ .ToList(); ++ ++ var refs = dep.AdditionalReferences ++ .Select(r => compiledDeps.First(d => d.Dep.AssemblyName == r).IlPath) ++ .ToList(); ++ ++ string ilPath = compiler.CompileAssembly(dep.AssemblyName, sources, refs, ++ features: dep.Features.Count > 0 ? dep.Features : null); ++ compiledDeps.Add((dep, ilPath)); ++ } ++ ++ // Step 2: Compile main assembly with Roslyn ++ var mainSources = new List ++ { ++ R2RTestCaseCompiler.ReadEmbeddedSource(testCase.MainSourceResourceName) ++ }; ++ if (testCase.MainExtraSourceResourceNames is not null) ++ { ++ foreach (string extra in testCase.MainExtraSourceResourceNames) ++ mainSources.Add(R2RTestCaseCompiler.ReadEmbeddedSource(extra)); ++ } ++ ++ var mainRefs = compiledDeps.Select(d => d.IlPath).ToList(); ++ string mainIlPath = compiler.CompileAssembly(testCase.Name, mainSources, mainRefs, ++ outputKind: Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary, ++ features: testCase.Expectations.Features.Count > 0 ? testCase.Expectations.Features : null); ++ ++ // Step 3: Crossgen2 dependencies ++ var driver = new R2RDriver(); ++ var allRefPaths = BuildReferencePaths(ilDir); ++ ++ foreach (var (dep, ilPath) in compiledDeps) ++ { ++ if (!dep.Crossgen) ++ continue; ++ ++ string r2rPath = Path.Combine(r2rDir, Path.GetFileName(ilPath)); ++ var result = driver.Compile(new R2RCompilationOptions ++ { ++ InputPath = ilPath, ++ OutputPath = r2rPath, ++ ReferencePaths = allRefPaths, ++ ExtraArgs = dep.CrossgenOptions, ++ }); ++ ++ Assert.True(result.Success, ++ $"crossgen2 failed for dependency '{dep.AssemblyName}':\n{result.StandardError}\n{result.StandardOutput}"); ++ } ++ ++ // Step 4: Crossgen2 main assembly ++ string mainR2RPath = Path.Combine(r2rDir, Path.GetFileName(mainIlPath)); ++ ++ if (testCase.Expectations.CompositeMode) ++ { ++ RunCompositeCompilation(testCase, driver, ilDir, r2rDir, mainIlPath, mainR2RPath, allRefPaths, compiledDeps); ++ } ++ else ++ { ++ RunSingleCompilation(testCase, driver, mainIlPath, mainR2RPath, allRefPaths); ++ } ++ ++ // Step 5: Validate R2R output ++ var checker = new R2RResultChecker(); ++ checker.Check(mainR2RPath, testCase.Expectations); ++ } ++ finally ++ { ++ // Keep temp directory for debugging if KEEP_R2R_TESTS env var is set ++ if (Environment.GetEnvironmentVariable("KEEP_R2R_TESTS") is null) ++ { ++ try { Directory.Delete(tempDir, true); } ++ catch { /* best effort */ } ++ } ++ } ++ } ++ ++ private static void RunSingleCompilation( ++ R2RTestCase testCase, ++ R2RDriver driver, ++ string mainIlPath, ++ string mainR2RPath, ++ List allRefPaths) ++ { ++ var options = new R2RCompilationOptions ++ { ++ InputPath = mainIlPath, ++ OutputPath = mainR2RPath, ++ ReferencePaths = allRefPaths, ++ ExtraArgs = testCase.Expectations.Crossgen2Options.ToList(), ++ }; ++ ++ var result = driver.Compile(options); ++ Assert.True(result.Success, ++ $"crossgen2 failed for main assembly '{testCase.Name}':\n{result.StandardError}\n{result.StandardOutput}"); ++ } ++ ++ private static void RunCompositeCompilation( ++ R2RTestCase testCase, ++ R2RDriver driver, ++ string ilDir, ++ string r2rDir, ++ string mainIlPath, ++ string mainR2RPath, ++ List allRefPaths, ++ List<(DependencyInfo Dep, string IlPath)> compiledDeps) ++ { ++ var compositeInputs = new List { mainIlPath }; ++ foreach (var (dep, ilPath) in compiledDeps) ++ { ++ if (dep.Crossgen) ++ compositeInputs.Add(ilPath); ++ } ++ ++ var options = new R2RCompilationOptions ++ { ++ InputPath = mainIlPath, ++ OutputPath = mainR2RPath, ++ ReferencePaths = allRefPaths, ++ Composite = true, ++ CompositeInputPaths = compositeInputs, ++ ExtraArgs = testCase.Expectations.Crossgen2Options.ToList(), ++ }; ++ ++ var result = driver.Compile(options); ++ Assert.True(result.Success, ++ $"crossgen2 composite compilation failed for '{testCase.Name}':\n{result.StandardError}\n{result.StandardOutput}"); ++ } ++ ++ private static List BuildReferencePaths(string ilDir) ++ { ++ var paths = new List(); ++ ++ // Add all compiled IL assemblies as references ++ paths.Add(Path.Combine(ilDir, "*.dll")); ++ ++ // Add framework references (managed assemblies) ++ paths.Add(Path.Combine(TestPaths.RuntimePackDir, "*.dll")); ++ ++ // System.Private.CoreLib is in the native directory, not lib ++ string runtimePackDir = TestPaths.RuntimePackDir; ++ string nativeDir = Path.GetFullPath(Path.Combine(runtimePackDir, "..", "..", "native")); ++ if (Directory.Exists(nativeDir)) ++ { ++ string spcl = Path.Combine(nativeDir, "System.Private.CoreLib.dll"); ++ if (File.Exists(spcl)) ++ paths.Add(spcl); ++ } ++ ++ return paths; ++ } ++} +diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs +new file mode 100644 +index 00000000000..2524ae4fd86 +--- /dev/null ++++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs +@@ -0,0 +1,167 @@ ++// Licensed to the .NET Foundation under one or more agreements. ++// The .NET Foundation licenses this file to you under the MIT license. ++ ++using System; ++using System.Collections.Generic; ++using System.IO; ++using System.Runtime.InteropServices; ++using System.Text.RegularExpressions; ++ ++namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; ++ ++/// ++/// Provides paths to build artifacts needed by the test infrastructure. ++/// All paths come from RuntimeHostConfigurationOption items in the csproj. ++/// ++internal static class TestPaths ++{ ++ private static string GetRequiredConfig(string key) ++ { ++ return AppContext.GetData(key) as string ++ ?? throw new InvalidOperationException($"Missing RuntimeHostConfigurationOption '{key}'. Was the project built with the correct properties?"); ++ } ++ ++ /// ++ /// Path to the crossgen2 output directory (contains crossgen2.dll and clrjit). ++ /// e.g. artifacts/bin/coreclr/linux.x64.Checked/crossgen2/ ++ /// Falls back to Checked or Release if Debug path doesn't exist. ++ /// ++ public static string Crossgen2Dir ++ { ++ get ++ { ++ string dir = GetRequiredConfig("R2RTest.Crossgen2Dir"); ++ if (!Directory.Exists(dir)) ++ { ++ // Try Checked and Release fallbacks since crossgen2 may be built in a different config ++ foreach (string fallbackConfig in new[] { "Checked", "Release", "Debug" }) ++ { ++ string fallback = Regex.Replace( ++ dir, @"\.(Debug|Release|Checked)[/\\]", $".{fallbackConfig}/"); ++ if (Directory.Exists(fallback)) ++ return fallback; ++ } ++ } ++ ++ return dir; ++ } ++ } ++ ++ /// ++ /// Path to the crossgen2.dll managed assembly. ++ /// ++ public static string Crossgen2Dll => Path.Combine(Crossgen2Dir, "crossgen2.dll"); ++ ++ /// ++ /// Path to the runtime pack managed assemblies directory. ++ /// e.g. artifacts/bin/microsoft.netcore.app.runtime.linux-x64/Release/runtimes/linux-x64/lib/net11.0/ ++ /// Falls back to Release if Debug path doesn't exist (libs are typically built Release). ++ /// ++ public static string RuntimePackDir ++ { ++ get ++ { ++ string dir = GetRequiredConfig("R2RTest.RuntimePackDir"); ++ if (!Directory.Exists(dir) && dir.Contains("Debug")) ++ { ++ string releaseFallback = dir.Replace("Debug", "Release"); ++ if (Directory.Exists(releaseFallback)) ++ return releaseFallback; ++ } ++ ++ return dir; ++ } ++ } ++ ++ /// ++ /// Path to the CoreCLR artifacts directory (contains native bits like corerun). ++ /// e.g. artifacts/bin/coreclr/linux.x64.Checked/ ++ /// Falls back to Checked or Release if Debug path doesn't exist. ++ /// ++ public static string CoreCLRArtifactsDir ++ { ++ get ++ { ++ string dir = GetRequiredConfig("R2RTest.CoreCLRArtifactsDir"); ++ if (!Directory.Exists(dir)) ++ { ++ foreach (string fallbackConfig in new[] { "Checked", "Release", "Debug" }) ++ { ++ string fallback = Regex.Replace( ++ dir, @"\.(Debug|Release|Checked)(/|\\|$)", $".{fallbackConfig}$2"); ++ if (Directory.Exists(fallback)) ++ return fallback; ++ } ++ } ++ ++ return dir; ++ } ++ } ++ ++ public static string TargetArchitecture => GetRequiredConfig("R2RTest.TargetArchitecture"); ++ public static string TargetOS => GetRequiredConfig("R2RTest.TargetOS"); ++ public static string Configuration => GetRequiredConfig("R2RTest.Configuration"); ++ ++ /// ++ /// Path to the reference assembly pack (for Roslyn compilation). ++ /// e.g. artifacts/bin/microsoft.netcore.app.ref/ref/net11.0/ ++ /// ++ public static string RefPackDir ++ { ++ get ++ { ++ string dir = GetRequiredConfig("R2RTest.RefPackDir"); ++ if (!Directory.Exists(dir)) ++ { ++ // Try the artifacts/bin/ref/net* fallback ++ string artifactsBin = Path.GetFullPath(Path.Combine(CoreCLRArtifactsDir, "..", "..")); ++ string refDir = Path.Combine(artifactsBin, "ref"); ++ if (Directory.Exists(refDir)) ++ { ++ foreach (string subDir in Directory.GetDirectories(refDir, "net*")) ++ { ++ if (File.Exists(Path.Combine(subDir, "System.Runtime.dll"))) ++ return subDir; ++ } ++ } ++ } ++ ++ return dir; ++ } ++ } ++ ++ /// ++ /// Returns the dotnet host executable path suitable for running crossgen2. ++ /// ++ public static string DotNetHost ++ { ++ get ++ { ++ string repoRoot = Path.GetFullPath(Path.Combine(CoreCLRArtifactsDir, "..", "..", "..", "..")); ++ string dotnetDir = Path.Combine(repoRoot, ".dotnet"); ++ string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; ++ string path = Path.Combine(dotnetDir, exe); ++ if (File.Exists(path)) ++ return path; ++ ++ // Fallback to PATH ++ return exe; ++ } ++ } ++ ++ /// ++ /// Returns the target triple string for crossgen2 (e.g. "linux-x64"). ++ /// ++ public static string TargetTriple => $"{TargetOS.ToLowerInvariant()}-{TargetArchitecture.ToLowerInvariant()}"; ++ ++ /// ++ /// Returns all framework reference assembly paths (*.dll in the runtime pack). ++ /// ++ public static IEnumerable GetFrameworkReferencePaths() ++ { ++ if (!Directory.Exists(RuntimePackDir)) ++ throw new DirectoryNotFoundException($"Runtime pack directory not found: {RuntimePackDir}"); ++ ++ return Directory.EnumerateFiles(RuntimePackDir, "*.dll"); ++ } ++} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs index c3c4ebcb2c2d04..b17643fb9eb99b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs @@ -10,9 +10,6 @@ public static class Utility { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetValue() => 42; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetName() => "hello"; } public class GenericWrapperA From b6d522740f660fcfc04f366544680f624578b5d0 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:17:00 -0700 Subject: [PATCH 25/74] Remove session state and planning files from tracking These are local session artifacts that should not be in the repository. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../plan.md | 118 - Composite Mode Async Thunks in R2R.md | 28 - CrossModuleResolutionTestPlan.md | 479 ---- r2r-tests-full.diff | 1987 ----------------- 4 files changed, 2612 deletions(-) delete mode 100644 .copilot/session-state/882aaa64-a620-4614-99b2-c7ab0c584744/plan.md delete mode 100644 Composite Mode Async Thunks in R2R.md delete mode 100644 CrossModuleResolutionTestPlan.md delete mode 100644 r2r-tests-full.diff diff --git a/.copilot/session-state/882aaa64-a620-4614-99b2-c7ab0c584744/plan.md b/.copilot/session-state/882aaa64-a620-4614-99b2-c7ab0c584744/plan.md deleted file mode 100644 index 10ebfff5f0caa8..00000000000000 --- a/.copilot/session-state/882aaa64-a620-4614-99b2-c7ab0c584744/plan.md +++ /dev/null @@ -1,118 +0,0 @@ -# R2R Cross-Module × Async × Composite Test Plan - -## Problem - -We need comprehensive test coverage for the intersection of three features in crossgen2 R2R output: -1. **Cross-module inlining** — methods from one assembly inlined into another -2. **Runtime-async** — async method compilation with [ASYNC] variants, continuation layouts, resumption stubs -3. **Composite mode** — multiple assemblies merged into one R2R image - -Each combination exercises different code paths in crossgen2 (MutableModule token encoding, manifest metadata, inlining info sections, continuation layout fixups) and the runtime (LoadDynamicInfoEntry, assembly loading, fixup resolution). - -## Existing Tests (9, all passing) - -| # | Test | Composite | Cross-Module | Async | Key Validations | -|---|------|-----------|-------------|-------|-----------------| -| 1 | BasicCrossModuleInlining | No | Direct sync | No | ManifestRef, CrossModuleInlinedMethod, CrossModuleInliningInfo | -| 2 | TransitiveReferences | No | Transitive sync | No | ManifestRef×2, CrossModuleInlinedMethod | -| 3 | AsyncCrossModuleInlining | No | Direct (async inlinee) | No¹ | ManifestRef, CrossModuleInlinedMethod | -| 4 | CompositeBasic | Yes | Implicit | No | ManifestRef | -| 5 | RuntimeAsyncMethodEmission | No | None | Yes | AsyncVariant×3 | -| 6 | RuntimeAsyncContinuationLayout | No | None | Yes | AsyncVariant, ContinuationLayout, ResumptionStubFixup | -| 7 | RuntimeAsyncDevirtualize | No | None | Yes | AsyncVariant | -| 8 | RuntimeAsyncNoYield | No | None | Yes | AsyncVariant×2 | -| 9 | RuntimeAsyncCrossModule | No | Direct | Yes | ManifestRef, AsyncVariant | - -¹ AsyncCrossModuleInlining inlines regular async methods (Task-returning) but does NOT use the runtime-async feature flag. - -## Coverage Gaps - -### Gap 1: Composite + detailed inlining validation -CompositeBasic only checks ManifestRef. No test validates that inlining actually occurs between assemblies in composite mode or checks InliningInfo2 / CrossModuleInlineInfo sections. - -### Gap 2: Composite + runtime-async -No test combines `--composite` with the `runtime-async` feature flag. - -### Gap 3: Composite + runtime-async + cross-module inlining -The full intersection — async methods from one assembly inlined into another within a composite image. Exercises MutableModule token encoding for cross-module async continuation layouts in composite metadata. - -### Gap 4: Non-composite runtime-async + cross-module inlining with continuations -RuntimeAsyncCrossModule validates ManifestRef + AsyncVariant but doesn't test --opt-cross-module inlining of async methods with GC refs across await points. - -### Gap 5: Transitive + async -No test combines transitive cross-module references with runtime-async. - -### Gap 6: Multi-step compilation -No test uses the multi-compilation model (compile composite first, then non-composite referencing those assemblies). - -### Gap 7: Composite + async devirtualization -RuntimeAsyncDevirtualize is single-assembly. Cross-module async devirtualization in composite is untested. - -### Gap 8: Input bubble boundaries in composite -No test uses --input-bubble + --inputbubbleref to test version bubble boundaries. - -## New Tests - -### Tier 1: Critical (unique code paths) - -**10. CompositeCrossModuleInlining** — Composite, sync, validates that Lib methods are inlined into Main -- Config: `--composite` -- Validates: ManifestRef, CrossModuleInlineInfo (CrossModuleInliningForCrossModuleDataOnly), InliningInfo2 -- Reuse existing source: BasicInlining.cs + InlineableLib.cs - -**11. CompositeAsync** — Composite + runtime-async baseline -- Config: `--composite`, `runtime-async=on` -- Validates: AsyncVariant for methods in both assemblies, ManifestRef -- New source: AsyncCompositeLib.cs + CompositeAsyncMain.cs - -**12. CompositeAsyncCrossModuleInlining** — THE full intersection test -- Config: `--composite`, `runtime-async=on` -- Validates: AsyncVariant, ContinuationLayout, CrossModuleInlineInfo, ManifestRef -- New source: needs async methods with GC refs across await in cross-module context - -**13. AsyncCrossModuleContinuation** — Non-composite, runtime-async + cross-module + continuations -- Config: `--opt-cross-module`, `runtime-async=on` -- Validates: AsyncVariant, ContinuationLayout, CrossModuleInlinedMethod, ManifestRef -- New source: AsyncDepLibContinuation.cs + AsyncCrossModuleContinuation.cs - -**14. MultiStepCompositeAndNonComposite** — Two-step compilation -- Step 1: Compile A+B as composite → validate composite output -- Step 2: Compile C non-composite with --ref A B --opt-cross-module A → validate C's output -- New source: needs a consumer assembly referencing composite-compiled libs - -### Tier 2: Important (depth coverage) - -**15. CompositeAsyncDevirtualize** — Composite + runtime-async + devirtualization across modules -- Config: `--composite`, `runtime-async=on` -- Validates: AsyncVariant for devirtualized calls -- New source: AsyncInterfaceLib.cs + CompositeAsyncDevirtMain.cs - -**16. CompositeTransitive** — Composite with 3 assemblies in A→B→C chain -- Config: `--composite` -- Validates: ManifestRef×3, CrossModuleInlineInfo for transitive inlining -- Reuse existing source: ExternalLib.cs + InlineableLibTransitive.cs + TransitiveReferences.cs - -**17. AsyncCrossModuleTransitive** — Non-composite, runtime-async + transitive cross-module -- Config: `--opt-cross-module`, `runtime-async=on` -- Validates: ManifestRef×2, AsyncVariant, CrossModuleInlinedMethod -- New source: AsyncExternalLib.cs + AsyncTransitiveLib.cs + AsyncTransitiveMain.cs - -### Tier 3: Extended coverage - -**18. CompositeAsyncTransitive** — Composite + async + transitive (3 assemblies) -- Config: `--composite`, `runtime-async=on` -- Validates: ManifestRef×3, AsyncVariant, CrossModuleInlineInfo -- Can reuse Tier 2 async transitive source with composite config - -**19. MultiStepCompositeAndNonCompositeAsync** — Multi-step with runtime-async -- Step 1: Compile AsyncLib+AsyncMain as composite -- Step 2: Compile AsyncConsumer non-composite with --opt-cross-module, runtime-async=on -- Validates: Both outputs have correct async variants and cross-module references - -## Notes - -- In composite mode, `--opt-cross-module` is NOT used — cross-module inlining is implicit -- Composite emits CrossModuleInlineInfo with `CrossModuleInliningForCrossModuleDataOnly` (cross-module entries only); non-composite emits `CrossModuleAllMethods` (all entries) -- InliningInfo2 is composite-only, per-module, same-module inlining -- For multi-step tests, crossgen2 reads IL metadata from --ref assemblies, not R2R output -- All tests validate R2R metadata only — they don't execute the compiled code diff --git a/Composite Mode Async Thunks in R2R.md b/Composite Mode Async Thunks in R2R.md deleted file mode 100644 index e84a55408c15f6..00000000000000 --- a/Composite Mode Async Thunks in R2R.md +++ /dev/null @@ -1,28 +0,0 @@ -# Composite Mode Async Thunks in R2R - -We unconditionally wrap MethodIL in MutableModuleWrappedMethodIL. - -At runtime, we disable any tokens pointing from MutableModule to any Module except for SPCL - -We also can't avoid creating MutableModule tokens for any TypeSystemEntities in the composite image. We inject methods -like Task.FromResult(T obj) where T could be from within the composite image. This would necessitate a new entry in -the MutableModule for the generic method with the definition pointing to SPCL, but the generic argument a token within -the composite image. - -We should start by enabling the AsyncVariantMethod method bodies. The real issue is the thunks, not the Async methods. - -After that I don't see a way forward to emit these thunks without enabling MutableModule references within the composite -image version bubble. This was explicitly forbidden in the original implementation for cross-module inlining, but I -don't exactly know why. If we can find exactly why (and hopefully it's not a hard restriction that can't be worked -around), we can build safeguards and tests to validate the safety of it. - -One alternative could be a special fixup that has the info required to construct the type without requiring module -tokens. Though a natural next step would be to deduplicate this information, which starts to look a lot like the -MutableModule. - -## Issue: How do we know we can resolve a type or method in different situations - -We may have Assembly A loaded and are doing eager fixups as we load Assembly B, then have a reference to a type in -Assembly D which goes through Assembly C. Do we need to load both assembly C and D? Are we able to do that in an eager -fixup while we load Assembly B? These are the types of issues we find - diff --git a/CrossModuleResolutionTestPlan.md b/CrossModuleResolutionTestPlan.md deleted file mode 100644 index 7d409f433b745c..00000000000000 --- a/CrossModuleResolutionTestPlan.md +++ /dev/null @@ -1,479 +0,0 @@ -# Cross-Module Reference Resolution Tests for R2R - -## Problem Statement - -The MutableModule's cross-module reference resolution has limited test coverage, especially for: -- Different assembly categories (version bubble, cross-module-inlineable-only, external/transitive) -- Composite mode (where `#:N` ModuleRef resolution is blocked by `m_pILModule == NULL`) -- ALC interactions (custom ALCs, composite single-ALC enforcement, JIT fallback) -- Edge cases (nested types, type forwarders, mixed-origin generics, field/method refs) - -We need tests that cover every path through `GetNonNestedResolutionScope` and the corresponding -runtime resolution in `NativeManifestModule`, for both single-assembly and composite R2R modes. - -## Background: Five Assembly Categories - -| Category | In Compilation Set | In Version Bubble | CrossModuleInlineable | -|----------|:--:|:--:|:--:| -| A. Compilation Module | ✅ | ✅ | ✅ | -| B. Version Bubble (`--inputbubbleref`) | ❌ | ✅ | ✅ | -| C. CrossModule-Only (`--opt-cross-module`) | ❌ | ❌ | ✅ | -| D. External (transitive dep) | ❌ | ❌ | ❌ | -| E. CoreLib | Special | Special | Special | - -### Critical Decision Tree for Type Tokenization - -- `VersionsWithType(type) == true` → original TypeDef from type's module → `MODULE_ZAPSIG` in fixup → **works everywhere** -- `VersionsWithType(type) == false` → MutableModule creates TypeRef → resolution scope is ModuleRef: - - CoreLib → `"System.Private.CoreLib"` → **works everywhere** - - CrossModuleInlineable/VersionsWithModule → `#:N` → **works non-composite, fails composite** - - External → `#AssemblyName:N` → **works non-composite, fails composite** - -### Why `#:N` ModuleRef Instead of AssemblyRef - -`NativeManifestModule` is `ModuleBase` (not `Module`) — it has no `PEAssembly`, no `Assembly`, -no ALC binder. It cannot independently resolve AssemblyRefs. `LoadAssemblyImpl` throws -`COR_E_BADIMAGEFORMAT` unconditionally. The `#:N` format routes loading through `m_pILModule` -(a real Module with an ALC binder). The `#AssemblyName:N` format preserves ALC-chaining through -intermediate modules. - ---- - -## Existing Infrastructure - -### 1. `src/tests/readytorun/tests/mainv1.csproj` — Cross-module inlining pattern -- Multiple assemblies compiled with explicit crossgen2 precommands -- Uses `--opt-cross-module:test`, `--map`, custom ALC loading -- `CLRTestBatchPreCommands`/`CLRTestBashPreCommands` pattern - -### 2. `src/tests/readytorun/crossboundarylayout/` — Composite mode matrix -- Shell driver with composite/inputbubble/single mode permutations -- Focused on field layout, not cross-module references - -### 3. `ILCompiler.Reflection.ReadyToRun` — Programmatic R2R reader -- `ReadyToRunReader`: `Methods`, `ImportSections`, `ManifestReferenceAssemblies` -- `ReadyToRunMethod`: `Fixups` → `FixupCell` → `ReadyToRunSignature` -- `InliningInfoSection2`: Cross-module inlining records with module indices -- `ReadyToRunSignature.ToString()`: Renders MODULE_ZAPSIG, ModuleOverride - -### 4. R2RDump CLI -- `--header --sc` dumps section contents including InliningInfo -- `--in ` with `-r ` for resolving references -- Text output only (no JSON), but parseable - -### 5. `CLRTest.CrossGen.targets` auto-R2RDump -- Infrastructure runs `R2RDump --header --sc --val` after crossgen2 automatically - ---- - -## R2R Compilation Validation Infrastructure - -### Goal -Validate that cross-module inlining **actually occurred** and that the expected fixup -signatures reference the expected external modules. - -### What the `--map` flag does NOT do -The crossgen2 `--map` flag produces a symbol/section layout map (RVA, length, node type) — a -linker-style map. It does **not** contain fixup signature details, MODULE_OVERRIDE references, -or inlining information. Existing tests (mainv1) use it only to confirm crossgen2 ran successfully. - -### Approach: R2RDump for Compile-Time Validation - -R2RDump (located at `src/coreclr/tools/r2rdump/`) is a CLI tool that reads R2R images and dumps -their contents, including import sections, fixup signatures, and inlining info. It uses the -`ILCompiler.Reflection.ReadyToRun` library internally. - -#### Strategy 1: R2RDump Section Contents (compile-time, in precommands) -Run R2RDump with `--sc` (section contents) after crossgen2 to dump the `InliningInfo2` section. -Parse the text output to verify cross-module inliner/inlinee relationships with module indices. - -```bash -# Run R2RDump after crossgen2 in the precommands -"$CORE_ROOT"/crossgen2/r2rdump --in main.dll --sc --rp "$CORE_ROOT" --rp . > main.r2rdump - -# Verify the InliningInfo2 section contains cross-module entries referencing assemblyC -grep -q "module assemblyC" main.r2rdump || (echo "FAIL: no cross-module inlining from assemblyC" && exit 1) -``` - -The `InliningInfoSection2` decoder in R2RDump outputs lines like: -``` -Inliners for inlinee 06000003 (module assemblyC): - 06000001 -``` -This shows that a method from `assemblyC` was inlined into `main`, confirming cross-module -inlining occurred at compile time. - -#### Strategy 2: Runtime Correctness -Test methods call cross-module inlined code and verify return values. If fixups resolved -correctly, the methods return expected values. This proves end-to-end correctness. - -#### Strategy 3: Programmatic Validation via ILCompiler.Reflection.ReadyToRun (future Phase 5) -For deeper validation, a managed test can reference `ILCompiler.Reflection.ReadyToRun` and use: -- `ReadyToRunReader.ManifestReferenceAssemblies` — verify expected assemblies in manifest -- `ReadyToRunMethod.Fixups` → `FixupCell.Signature` → `ReadyToRunSignature.ToString()` — verify MODULE_OVERRIDE references -- `InliningInfoSection2` — verify cross-module inlining records with module indices - -This is more robust than text-parsing R2RDump output but requires building a managed validation -tool. Deferred to Phase 5. - -### Validation Points Per Test Mode - -| Validation | What It Proves | -|------------|---------------| -| R2RDump InliningInfo2 shows `module assemblyC` | Crossgen2 performed cross-module inlining | -| R2RDump ManifestMetadata lists assemblyC | Manifest AssemblyRef table includes the dependency | -| Test methods return correct values at runtime | Fixups resolved successfully end-to-end | -| Crossgen2 `--map` file exists | Crossgen2 ran successfully (basic sanity) | - ---- - -## Test Design - -### Assembly Graph - -``` -Assembly A (main, compilation target) -├── References B (version bubble, --inputbubbleref) -├── References C (cross-module-inlineable only, --opt-cross-module:assemblyC) -│ ├── C references D (external, transitive dependency) -│ └── C references CoreLib -└── References CoreLib - -Assembly B (version bubble) -├── Defines types for version-bubble testing -└── References CoreLib - -Assembly C (cross-module-inlineable, NOT in version bubble) -├── Defines inlineable methods that reference: -│ ├── C's own types -│ ├── D.DType (transitive dependency) -│ ├── D.Outer.Inner (nested type) -│ ├── CoreLib types (List, etc.) -│ └── Type forwarded types -└── References D - -Assembly D (external, NOT in any set) -├── Defines types referenced transitively through C -├── Defines nested types (D.Outer.Inner) -└── Defines types used as generic arguments - -Assembly E (type forwarder source) -├── Has TypeForwarder for SomeForwardedType → D -└── C references SomeForwardedType via E's forwarder -``` - -### File Structure - -``` -src/tests/readytorun/crossmoduleresolution/ -├── main/ -│ ├── main.cs — Test driver with all test methods -│ ├── main.csproj — Main project (basic build, no crossgen2) -│ ├── main_crossmodule.csproj — Single R2R with --opt-cross-module:assemblyC -│ └── main_bubble.csproj — Single R2R with --inputbubbleref assemblyB -├── assemblyB/ -│ ├── B.cs — Version bubble types -│ └── assemblyB.csproj -├── assemblyC/ -│ ├── C.cs — Cross-module-inlineable methods + types -│ └── assemblyC.csproj -├── assemblyD/ -│ ├── D.cs — External/transitive types, nested types -│ └── assemblyD.csproj -├── assemblyE/ -│ ├── E.cs — TypeForwarder assembly -│ └── assemblyE.csproj -└── README.md — Test documentation -``` - -### Assembly Source Code - -#### Assembly D (`assemblyD/D.cs`) -```csharp -namespace AssemblyD -{ - public class DType { public int Value => 42; } - - public class DClass - { - public static int StaticField = 100; - public static int StaticMethod() => StaticField + 1; - } - - public class Outer - { - public class Inner - { - public int GetValue() => 99; - } - } - - public class SomeForwardedType - { - public static string Name => "forwarded"; - } -} -``` - -#### Assembly E (`assemblyE/E.cs`) -```csharp -using System.Runtime.CompilerServices; - -[assembly: TypeForwardedTo(typeof(AssemblyD.SomeForwardedType))] -``` - -#### Assembly B (`assemblyB/B.cs`) -```csharp -namespace AssemblyB -{ - public class BType { public int Value => 7; } - - public class BClass - { - public static int StaticMethod() => 77; - public static int StaticField = 777; - } -} -``` - -#### Assembly C (`assemblyC/C.cs`) -```csharp -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace AssemblyC -{ - public class CType { public int Value => 3; } - - public class CClass - { - public static int StaticField = 50; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int UseOwnType() => new CType().Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int UseDType() => new AssemblyD.DType().Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int CallDMethod() => AssemblyD.DClass.StaticMethod(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ReadDField() => AssemblyD.DClass.StaticField; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int UseNestedType() => new AssemblyD.Outer.Inner().GetValue(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string UseForwardedType() => AssemblyD.SomeForwardedType.Name; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int UseGenericWithDType() - { - var list = new List(); - list.Add(new AssemblyD.DType()); - return list[0].Value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int UseCoreLibGeneric() - { - var list = new List { 1, 2, 3 }; - return list.Count; - } - } - - public class CGeneric - { - public T Value { get; set; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetCount() => 1; - } - - public interface ICrossModule - { - int DoWork(); - } -} -``` - -#### Main Test Driver (`main/main.cs`) -```csharp -using System; -using System.IO; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.Loader; -using Xunit; - -// Interface implementation for cross-module dispatch test -class CrossModuleImpl : AssemblyC.ICrossModule -{ - public int DoWork() => 42; -} - -public static class CrossModuleResolutionTests -{ - // --- Version Bubble Tests (B) --- - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestTypeRef_VersionBubble() => AssemblyB.BClass.StaticMethod(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestFieldAccess_VersionBubble() => AssemblyB.BClass.StaticField; - - // --- Cross-Module-Only Tests (C) --- - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestTypeRef_CrossModuleOnly() => AssemblyC.CClass.UseOwnType(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestTypeRef_Transitive() => AssemblyC.CClass.UseDType(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestMethodCall_Transitive() => AssemblyC.CClass.CallDMethod(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestFieldAccess_Transitive() => AssemblyC.CClass.ReadDField(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestNestedType_External() => AssemblyC.CClass.UseNestedType(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static string TestTypeForwarder() => AssemblyC.CClass.UseForwardedType(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestGeneric_MixedOrigin() => AssemblyC.CClass.UseGenericWithDType(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestGeneric_CoreLib() => AssemblyC.CClass.UseCoreLibGeneric(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestGeneric_CrossModuleDefinition() => AssemblyC.CGeneric.GetCount(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestFieldAccess_CrossModule() => AssemblyC.CClass.StaticField; - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestInterfaceDispatch_CrossModule() - { - AssemblyC.ICrossModule impl = new CrossModuleImpl(); - return impl.DoWork(); - } - - // --- ALC Tests --- - - class TestLoadContext : AssemblyLoadContext - { - public TestLoadContext() : base( - AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()).IsCollectible) - { } - - public void TestLoadInSeparateALC() - { - // Load main assembly in a different ALC — R2R should still work (or JIT fallback) - Assembly a = LoadFromAssemblyPath( - Path.Combine(Directory.GetCurrentDirectory(), "main.dll")); - Assert.AreEqual(GetLoadContext(a), this); - } - - protected override Assembly Load(AssemblyName an) => null; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - static void TestALC_CustomLoad() => new TestLoadContext().TestLoadInSeparateALC(); - - // --- Entry Point --- - - [Fact] - public static int TestEntryPoint() - { - // Version bubble - Assert.AreEqual(77, TestTypeRef_VersionBubble()); - Assert.AreEqual(777, TestFieldAccess_VersionBubble()); - - // Cross-module-only (C's inlined methods) - Assert.AreEqual(3, TestTypeRef_CrossModuleOnly()); - Assert.AreEqual(42, TestTypeRef_Transitive()); - Assert.AreEqual(101, TestMethodCall_Transitive()); - Assert.AreEqual(100, TestFieldAccess_Transitive()); - Assert.AreEqual(99, TestNestedType_External()); - Assert.AreEqual("forwarded", TestTypeForwarder()); - Assert.AreEqual(42, TestGeneric_MixedOrigin()); - Assert.AreEqual(3, TestGeneric_CoreLib()); - Assert.AreEqual(1, TestGeneric_CrossModuleDefinition()); - Assert.AreEqual(50, TestFieldAccess_CrossModule()); - Assert.AreEqual(42, TestInterfaceDispatch_CrossModule()); - - // ALC - TestALC_CustomLoad(); - - return 100; // success - } -} -``` - -### Test Modes (Separate .csproj Files) - -#### Mode 1: `main_crossmodule.csproj` — Single R2R + `--opt-cross-module` - -Crossgen2 precommands: -1. Copy IL DLLs to `IL_DLLS/` -2. Crossgen2 assemblyD (no special flags) -3. Crossgen2 assemblyB (no special flags) -4. Crossgen2 assemblyC with `-r assemblyD.dll` -5. Crossgen2 main with `--opt-cross-module:assemblyC --map -r assemblyB.dll -r assemblyC.dll -r assemblyD.dll` -6. **Validate**: check map file for MODULE_OVERRIDE references to assemblyC - -#### Mode 2: `main_bubble.csproj` — Single R2R + `--inputbubbleref` - -Crossgen2 precommands: -1. Copy IL DLLs to `IL_DLLS/` -2. Crossgen2 assemblyB (no special flags) -3. Crossgen2 main with `--inputbubbleref assemblyB --map -r assemblyB.dll -r assemblyC.dll -r assemblyD.dll` -4. **Validate**: check map file for MODULE_ZAPSIG references to assemblyB - ---- - -## Phases - -### Phase 1: Scaffolding (Current Scope) -1. Create Assembly D — external types, nested types, forwarded type definition -2. Create Assembly E — TypeForwarder to D -3. Create Assembly B — version bubble types -4. Create Assembly C — cross-module-inlineable methods with `[AggressiveInlining]` -5. Create main test driver with all test methods - -### Phase 2: Single-Assembly R2R Tests (Current Scope) -6. Create `main_crossmodule.csproj` with crossgen2 precommands + map file validation -7. Create `main_bubble.csproj` with crossgen2 precommands + map file validation -8. Build and run — verify all tests pass in single R2R mode - -### Phase 3: Composite Mode Tests (Deferred) -9. Create `main_composite.csproj` — `--composite` with A+B -10. Create `main_composite_crossmodule.csproj` — `--composite` A+B + `--opt-cross-module:assemblyC` -11. Determine expected behavior — should composite+crossmodule fail at crossgen2 time, at runtime, or JIT fallback? -12. Build and run — verify composite tests - -### Phase 4: ALC Tests (Deferred) -13. Add ALC test cases — custom ALC loading, composite ALC mismatch fallback to JIT -14. Build and run — verify ALC scenarios - -### Phase 5: Programmatic R2R Validation (Deferred) -15. Create managed validation tool using `ILCompiler.Reflection.ReadyToRun` -16. Validate `ManifestReferenceAssemblies` contains expected assemblies -17. Validate `ReadyToRunMethod.Fixups` contain expected MODULE_OVERRIDE signatures -18. Validate `InliningInfoSection2` records cross-module inlining with correct module indices - ---- - -## Design Decisions - -- **AggressiveInlining**: Yes — `[MethodImpl(AggressiveInlining)]` on C's methods to force cross-module inlining -- **Composite + cross-module behavior**: Eventually should be fixed (probably JIT fallback). Deferred to Phase 3 -- **Test organization**: Separate .csproj files per mode (mainv1/mainv2 pattern) -- **Pre-commands**: Both Windows batch AND bash (following existing convention) -- **Priority**: Pri1 — specialized cross-module tests -- **Validation**: Map file + R2RDump for Phase 1-2; programmatic ILCompiler.Reflection.ReadyToRun for Phase 5 -- **Return code**: 100 = success (matching CoreCLR test convention) diff --git a/r2r-tests-full.diff b/r2r-tests-full.diff deleted file mode 100644 index f866aac6343571..00000000000000 --- a/r2r-tests-full.diff +++ /dev/null @@ -1,1987 +0,0 @@ -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs -new file mode 100644 -index 00000000000..8fe039762c1 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/Expectations/R2RExpectationAttributes.cs -@@ -0,0 +1,123 @@ -+// Licensed to the .NET Foundation under one or more agreements. -+// The .NET Foundation licenses this file to you under the MIT license. -+ -+using System; -+using System.Diagnostics; -+ -+namespace ILCompiler.ReadyToRun.Tests.Expectations; -+ -+/// -+/// Marks a method as expected to be cross-module inlined into the main R2R image. -+/// The R2R result checker will verify a CHECK_IL_BODY fixup exists for this method's callee. -+/// -+[Conditional("R2R_EXPECTATIONS")] -+[AttributeUsage(AttributeTargets.Method)] -+public sealed class ExpectInlinedAttribute : Attribute -+{ -+ /// -+ /// The fully qualified name of the method expected to be inlined. -+ /// If null, infers from the method body (looks for the first cross-module call). -+ /// -+ public string? MethodName { get; set; } -+} -+ -+/// -+/// Marks a method as expected to have a specific number of RuntimeFunctions in the R2R image. -+/// -+[Conditional("R2R_EXPECTATIONS")] -+[AttributeUsage(AttributeTargets.Method)] -+public sealed class ExpectRuntimeFunctionCountAttribute : Attribute -+{ -+ public int ExpectedCount { get; set; } -+} -+ -+/// -+/// Declares that the R2R image should contain a manifest reference to the specified assembly. -+/// -+[Conditional("R2R_EXPECTATIONS")] -+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] -+public sealed class ExpectManifestRefAttribute : Attribute -+{ -+ public string AssemblyName { get; } -+ -+ public ExpectManifestRefAttribute(string assemblyName) -+ { -+ AssemblyName = assemblyName; -+ } -+} -+ -+/// -+/// Specifies a crossgen2 command-line option for the main assembly compilation. -+/// Applied at the assembly level of the test case. -+/// -+[Conditional("R2R_EXPECTATIONS")] -+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] -+public sealed class Crossgen2OptionAttribute : Attribute -+{ -+ public string Option { get; } -+ -+ public Crossgen2OptionAttribute(string option) -+ { -+ Option = option; -+ } -+} -+ -+/// -+/// Declares a dependency assembly that should be compiled before the main test assembly. -+/// The source files are compiled with Roslyn, then optionally crossgen2'd before the main assembly. -+/// -+[Conditional("R2R_EXPECTATIONS")] -+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] -+public sealed class SetupCompileBeforeAttribute : Attribute -+{ -+ /// -+ /// The output assembly filename (e.g., "InlineableLib.dll"). -+ /// -+ public string OutputName { get; } -+ -+ /// -+ /// Source file paths relative to the test case's Dependencies/ folder. -+ /// -+ public string[] SourceFiles { get; } -+ -+ /// -+ /// Additional assembly references needed to compile this dependency. -+ /// -+ public string[]? References { get; set; } -+ -+ /// -+ /// If true, this assembly is also crossgen2'd before the main assembly. -+ /// -+ public bool Crossgen { get; set; } -+ -+ /// -+ /// Additional crossgen2 options for this dependency assembly. -+ /// -+ public string[]? CrossgenOptions { get; set; } -+ -+ public SetupCompileBeforeAttribute(string outputName, string[] sourceFiles) -+ { -+ OutputName = outputName; -+ SourceFiles = sourceFiles; -+ } -+} -+ -+/// -+/// Marks a method as expected to have R2R compiled code in the output image. -+/// -+[Conditional("R2R_EXPECTATIONS")] -+[AttributeUsage(AttributeTargets.Method)] -+public sealed class ExpectR2RMethodAttribute : Attribute -+{ -+} -+ -+/// -+/// Marks an assembly-level option to enable composite mode compilation. -+/// When present, all SetupCompileBefore assemblies with Crossgen=true are compiled -+/// together with the main assembly using --composite. -+/// -+[Conditional("R2R_EXPECTATIONS")] -+[AttributeUsage(AttributeTargets.Assembly)] -+public sealed class CompositeModeAttribute : Attribute -+{ -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj -new file mode 100644 -index 00000000000..24c1d7f9a74 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj -@@ -0,0 +1,56 @@ -+ -+ -+ -+ ILCompiler.ReadyToRun.Tests -+ $(NetCoreAppToolCurrent) -+ enable -+ false -+ true -+ x64;x86 -+ AnyCPU -+ linux-x64;win-x64;osx-x64 -+ Debug;Release;Checked -+ true -+ -notrait category=failing -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ $(RuntimeBinDir)/crossgen2 -+ -+ -+ $(MicrosoftNetCoreAppRuntimePackRidLibTfmDir) -+ -+ -+ $(MicrosoftNetCoreAppRefPackRefDir) -+ -+ -+ $(CoreCLRArtifactsPath) -+ -+ -+ $(TargetArchitecture) -+ -+ -+ $(TargetOS) -+ -+ -+ $(Configuration) -+ -+ -+ -+ -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs -new file mode 100644 -index 00000000000..b9efb76fab2 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs -@@ -0,0 +1,27 @@ -+// Test: Async method thunks in R2R -+// Validates that runtime-async compiled methods produce the expected -+// RuntimeFunction layout (thunk + async body + resumption stub). -+using System; -+using System.Runtime.CompilerServices; -+using System.Threading.Tasks; -+ -+public static class AsyncMethods -+{ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task TestAsyncInline() -+ { -+ return await AsyncInlineableLib.GetValueAsync(); -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task TestAsyncStringInline() -+ { -+ return await AsyncInlineableLib.GetStringAsync(); -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static int TestSyncFromAsyncLib() -+ { -+ return AsyncInlineableLib.GetValueSync(); -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs -new file mode 100644 -index 00000000000..ba301272868 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs -@@ -0,0 +1,26 @@ -+// Test: Basic cross-module inlining -+// Validates that crossgen2 with --opt-cross-module produces CHECK_IL_BODY fixups -+// for methods inlined from InlineableLib into this main assembly. -+using System; -+using System.Runtime.CompilerServices; -+ -+public static class BasicInlining -+{ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static int TestGetValue() -+ { -+ return InlineableLib.GetValue(); -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static string TestGetString() -+ { -+ return InlineableLib.GetString(); -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static int TestAdd() -+ { -+ return InlineableLib.Add(10, 32); -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs -new file mode 100644 -index 00000000000..0ba24d55635 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs -@@ -0,0 +1,20 @@ -+// Test: Composite mode basic compilation -+// Validates that composite mode R2R compilation with multiple assemblies -+// produces correct manifest references and component assembly entries. -+using System; -+using System.Runtime.CompilerServices; -+ -+public static class CompositeBasic -+{ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static int TestCompositeCall() -+ { -+ return CompositeLib.GetCompositeValue(); -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static object TestCompositeTypeCreation() -+ { -+ return new CompositeLib.CompositeType(); -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs -new file mode 100644 -index 00000000000..153804e6279 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs -@@ -0,0 +1,15 @@ -+using System; -+using System.Runtime.CompilerServices; -+using System.Threading.Tasks; -+ -+public static class AsyncInlineableLib -+{ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static async Task GetValueAsync() => 42; -+ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static async Task GetStringAsync() => "Hello from async"; -+ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static int GetValueSync() => 42; -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs -new file mode 100644 -index 00000000000..2dc5db2de38 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs -@@ -0,0 +1,11 @@ -+using System; -+ -+public static class CompositeLib -+{ -+ public static int GetCompositeValue() => 100; -+ -+ public class CompositeType -+ { -+ public string Name { get; set; } = "Composite"; -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs -new file mode 100644 -index 00000000000..d56f2880564 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs -@@ -0,0 +1,19 @@ -+using System; -+ -+public static class ExternalLib -+{ -+ public static int ExternalValue => 99; -+ -+ public class ExternalType -+ { -+ public int Value { get; set; } -+ } -+ -+ public class Outer -+ { -+ public class Inner -+ { -+ public static int NestedValue => 77; -+ } -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs -new file mode 100644 -index 00000000000..a799cfed728 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs -@@ -0,0 +1,14 @@ -+using System; -+using System.Runtime.CompilerServices; -+ -+public static class InlineableLib -+{ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static int GetValue() => 42; -+ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static string GetString() => "Hello from InlineableLib"; -+ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static int Add(int a, int b) => a + b; -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs -new file mode 100644 -index 00000000000..15fd29dda19 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs -@@ -0,0 +1,14 @@ -+using System; -+using System.Runtime.CompilerServices; -+ -+public static class InlineableLibTransitive -+{ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static int GetExternalValue() => ExternalLib.ExternalValue; -+ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static int GetNestedValue() => ExternalLib.Outer.Inner.NestedValue; -+ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static ExternalLib.ExternalType CreateExternal() => new ExternalLib.ExternalType { Value = 42 }; -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs -new file mode 100644 -index 00000000000..25bac820fc3 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs -@@ -0,0 +1,26 @@ -+// Test: Transitive cross-module references -+// Validates that when InlineableLibTransitive is inlined, its references to ExternalLib -+// are properly encoded in the R2R image (requiring tokens for both libraries). -+using System; -+using System.Runtime.CompilerServices; -+ -+public static class TransitiveReferences -+{ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static int TestTransitiveValue() -+ { -+ return InlineableLibTransitive.GetExternalValue(); -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static int TestNestedTypeAccess() -+ { -+ return InlineableLibTransitive.GetNestedValue(); -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static object TestTransitiveTypeCreation() -+ { -+ return InlineableLibTransitive.CreateExternal(); -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs -new file mode 100644 -index 00000000000..620c7da3dc1 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs -@@ -0,0 +1,286 @@ -+// Licensed to the .NET Foundation under one or more agreements. -+// The .NET Foundation licenses this file to you under the MIT license. -+ -+using System.Collections.Generic; -+using ILCompiler.ReadyToRun.Tests.TestCasesRunner; -+using Internal.ReadyToRunConstants; -+using Xunit; -+ -+namespace ILCompiler.ReadyToRun.Tests.TestCases; -+ -+/// -+/// xUnit test suites for R2R cross-module resolution tests. -+/// Each test method builds assemblies with Roslyn, crossgen2's them, and validates the R2R output. -+/// -+public class R2RTestSuites -+{ -+ private static readonly KeyValuePair RuntimeAsyncFeature = new("runtime-async", "on"); -+ -+ [Fact] -+ public void BasicCrossModuleInlining() -+ { -+ var expectations = new R2RExpectations(); -+ expectations.ExpectedManifestRefs.Add("InlineableLib"); -+ expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetValue")); -+ expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetString")); -+ expectations.Crossgen2Options.Add("--opt-cross-module:InlineableLib"); -+ -+ var testCase = new R2RTestCase -+ { -+ Name = "BasicCrossModuleInlining", -+ MainSourceResourceName = "CrossModuleInlining/BasicInlining.cs", -+ Dependencies = new List -+ { -+ new DependencyInfo -+ { -+ AssemblyName = "InlineableLib", -+ SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/InlineableLib.cs" }, -+ Crossgen = true, -+ } -+ }, -+ Expectations = expectations, -+ }; -+ -+ new R2RTestRunner().Run(testCase); -+ } -+ -+ [Fact] -+ public void TransitiveReferences() -+ { -+ var expectations = new R2RExpectations(); -+ expectations.ExpectedManifestRefs.Add("InlineableLibTransitive"); -+ expectations.ExpectedManifestRefs.Add("ExternalLib"); -+ expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetExternalValue")); -+ expectations.Crossgen2Options.Add("--opt-cross-module:InlineableLibTransitive"); -+ -+ var testCase = new R2RTestCase -+ { -+ Name = "TransitiveReferences", -+ MainSourceResourceName = "CrossModuleInlining/TransitiveReferences.cs", -+ Dependencies = new List -+ { -+ new DependencyInfo -+ { -+ AssemblyName = "ExternalLib", -+ SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/ExternalLib.cs" }, -+ Crossgen = false, -+ }, -+ new DependencyInfo -+ { -+ AssemblyName = "InlineableLibTransitive", -+ SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/InlineableLibTransitive.cs" }, -+ Crossgen = true, -+ AdditionalReferences = { "ExternalLib" }, -+ } -+ }, -+ Expectations = expectations, -+ }; -+ -+ new R2RTestRunner().Run(testCase); -+ } -+ -+ [Fact] -+ public void AsyncCrossModuleInlining() -+ { -+ var expectations = new R2RExpectations(); -+ expectations.ExpectedManifestRefs.Add("AsyncInlineableLib"); -+ expectations.ExpectedInlinedMethods.Add(new ExpectedInlinedMethod("GetValueAsync")); -+ expectations.Crossgen2Options.Add("--opt-cross-module:AsyncInlineableLib"); -+ -+ var testCase = new R2RTestCase -+ { -+ Name = "AsyncCrossModuleInlining", -+ MainSourceResourceName = "CrossModuleInlining/AsyncMethods.cs", -+ Dependencies = new List -+ { -+ new DependencyInfo -+ { -+ AssemblyName = "AsyncInlineableLib", -+ SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/AsyncInlineableLib.cs" }, -+ Crossgen = true, -+ } -+ }, -+ Expectations = expectations, -+ }; -+ -+ new R2RTestRunner().Run(testCase); -+ } -+ -+ [Fact] -+ public void CompositeBasic() -+ { -+ var expectations = new R2RExpectations -+ { -+ CompositeMode = true, -+ }; -+ expectations.ExpectedManifestRefs.Add("CompositeLib"); -+ -+ var testCase = new R2RTestCase -+ { -+ Name = "CompositeBasic", -+ MainSourceResourceName = "CrossModuleInlining/CompositeBasic.cs", -+ Dependencies = new List -+ { -+ new DependencyInfo -+ { -+ AssemblyName = "CompositeLib", -+ SourceResourceNames = new[] { "CrossModuleInlining/Dependencies/CompositeLib.cs" }, -+ Crossgen = true, -+ } -+ }, -+ Expectations = expectations, -+ }; -+ -+ new R2RTestRunner().Run(testCase); -+ } -+ -+ /// -+ /// PR #124203: Async methods produce [ASYNC] variant entries with resumption stubs. -+ /// PR #121456: Resumption stubs are emitted as ResumptionStubEntryPoint fixups. -+ /// PR #123643: Methods with GC refs across awaits produce ContinuationLayout fixups. -+ /// -+ [Fact] -+ public void RuntimeAsyncMethodEmission() -+ { -+ string attrSource = R2RTestCaseCompiler.ReadEmbeddedSource( -+ "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"); -+ string mainSource = R2RTestCaseCompiler.ReadEmbeddedSource( -+ "RuntimeAsync/BasicAsyncEmission.cs"); -+ -+ var expectations = new R2RExpectations(); -+ expectations.Features.Add(RuntimeAsyncFeature); -+ expectations.ExpectedAsyncVariantMethods.Add("SimpleAsyncMethod"); -+ expectations.ExpectedAsyncVariantMethods.Add("AsyncVoidReturn"); -+ expectations.ExpectedAsyncVariantMethods.Add("ValueTaskMethod"); -+ -+ var testCase = new R2RTestCase -+ { -+ Name = "RuntimeAsyncMethodEmission", -+ MainSourceResourceName = "RuntimeAsync/BasicAsyncEmission.cs", -+ MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, -+ Dependencies = new List(), -+ Expectations = expectations, -+ }; -+ -+ new R2RTestRunner().Run(testCase); -+ } -+ -+ /// -+ /// PR #123643: Async methods capturing GC refs across await points -+ /// produce ContinuationLayout fixups encoding the GC ref map. -+ /// PR #124203: Resumption stubs for methods with suspension points. -+ /// -+ [Fact] -+ public void RuntimeAsyncContinuationLayout() -+ { -+ string attrSource = R2RTestCaseCompiler.ReadEmbeddedSource( -+ "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs"); -+ string mainSource = R2RTestCaseCompiler.ReadEmbeddedSource( -+ "RuntimeAsync/AsyncWithContinuation.cs"); -+ -+ var expectations = new R2RExpectations -+ { -+ ExpectContinuationLayout = true, -+ ExpectResumptionStubFixup = true, -+ }; -+ expectations.Features.Add(RuntimeAsyncFeature); -+ expectations.ExpectedAsyncVariantMethods.Add("CaptureObjectAcrossAwait"); -+ expectations.ExpectedAsyncVariantMethods.Add("CaptureMultipleRefsAcrossAwait"); -+ -+ var testCase = new R2RTestCase -+ { -+ Name = "RuntimeAsyncContinuationLayout", -+ MainSourceResourceName = "RuntimeAsync/AsyncWithContinuation.cs", -+ MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, -+ Dependencies = new List(), -+ Expectations = expectations, -+ }; -+ -+ new R2RTestRunner().Run(testCase); -+ } -+ -+ /// -+ /// PR #125420: Devirtualization of async methods through -+ /// AsyncAwareVirtualMethodResolutionAlgorithm. -+ /// -+ [Fact] -+ public void RuntimeAsyncDevirtualize() -+ { -+ var expectations = new R2RExpectations(); -+ expectations.Features.Add(RuntimeAsyncFeature); -+ expectations.ExpectedAsyncVariantMethods.Add("GetValueAsync"); -+ -+ var testCase = new R2RTestCase -+ { -+ Name = "RuntimeAsyncDevirtualize", -+ MainSourceResourceName = "RuntimeAsync/AsyncDevirtualize.cs", -+ MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, -+ Dependencies = new List(), -+ Expectations = expectations, -+ }; -+ -+ new R2RTestRunner().Run(testCase); -+ } -+ -+ /// -+ /// PR #124203: Async methods without yield points may omit resumption stubs. -+ /// Validates that no-yield async methods still produce [ASYNC] variants. -+ /// -+ [Fact] -+ public void RuntimeAsyncNoYield() -+ { -+ var expectations = new R2RExpectations(); -+ expectations.Features.Add(RuntimeAsyncFeature); -+ expectations.ExpectedAsyncVariantMethods.Add("AsyncButNoAwait"); -+ expectations.ExpectedAsyncVariantMethods.Add("AsyncWithConditionalAwait"); -+ -+ var testCase = new R2RTestCase -+ { -+ Name = "RuntimeAsyncNoYield", -+ MainSourceResourceName = "RuntimeAsync/AsyncNoYield.cs", -+ MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, -+ Dependencies = new List(), -+ Expectations = expectations, -+ }; -+ -+ new R2RTestRunner().Run(testCase); -+ } -+ -+ /// -+ /// PR #121679: MutableModule async references + cross-module inlining -+ /// of runtime-async methods with cross-module dependency. -+ /// -+ [Fact] -+ public void RuntimeAsyncCrossModule() -+ { -+ var expectations = new R2RExpectations(); -+ expectations.Features.Add(RuntimeAsyncFeature); -+ expectations.ExpectedManifestRefs.Add("AsyncDepLib"); -+ expectations.ExpectedAsyncVariantMethods.Add("CallCrossModuleAsync"); -+ expectations.Crossgen2Options.Add("--opt-cross-module:AsyncDepLib"); -+ -+ var testCase = new R2RTestCase -+ { -+ Name = "RuntimeAsyncCrossModule", -+ MainSourceResourceName = "RuntimeAsync/AsyncCrossModule.cs", -+ MainExtraSourceResourceNames = new[] { "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" }, -+ Dependencies = new List -+ { -+ new DependencyInfo -+ { -+ AssemblyName = "AsyncDepLib", -+ SourceResourceNames = new[] -+ { -+ "RuntimeAsync/Dependencies/AsyncDepLib.cs", -+ "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs" -+ }, -+ Crossgen = true, -+ Features = { RuntimeAsyncFeature }, -+ } -+ }, -+ Expectations = expectations, -+ }; -+ -+ new R2RTestRunner().Run(testCase); -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs -new file mode 100644 -index 00000000000..28534b5a9fc ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs -@@ -0,0 +1,28 @@ -+// Test: Cross-module async method inlining -+// Validates that async methods from a dependency library can be -+// cross-module inlined, creating manifest refs and CHECK_IL_BODY fixups. -+using System; -+using System.Runtime.CompilerServices; -+using System.Threading.Tasks; -+ -+public static class AsyncCrossModule -+{ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task CallCrossModuleAsync() -+ { -+ return await AsyncDepLib.GetValueAsync(); -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task CallCrossModuleStringAsync() -+ { -+ return await AsyncDepLib.GetStringAsync(); -+ } -+ -+ // Call a non-async sync method from async lib -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static int CallCrossModuleSync() -+ { -+ return AsyncDepLib.GetValueSync(); -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs -new file mode 100644 -index 00000000000..76d69eecf2f ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs -@@ -0,0 +1,56 @@ -+// Test: Async virtual method devirtualization in R2R -+// Validates that sealed class and interface dispatch of async methods -+// produces devirtualized direct call entries in the R2R image. -+using System; -+using System.Runtime.CompilerServices; -+using System.Threading.Tasks; -+ -+public interface IAsyncService -+{ -+ Task GetValueAsync(); -+} -+ -+public class OpenImpl : IAsyncService -+{ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public virtual async Task GetValueAsync() -+ { -+ await Task.Yield(); -+ return 10; -+ } -+} -+ -+public sealed class SealedImpl : IAsyncService -+{ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public async Task GetValueAsync() -+ { -+ await Task.Yield(); -+ return 20; -+ } -+} -+ -+public static class AsyncDevirtualize -+{ -+ // Sealed type known at compile time — should devirtualize -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task CallOnSealed(SealedImpl obj) -+ { -+ return await obj.GetValueAsync(); -+ } -+ -+ // newobj gives exact type info — should devirtualize through resolveVirtualMethod -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task CallOnNewOpen() -+ { -+ IAsyncService svc = new OpenImpl(); -+ return await svc.GetValueAsync(); -+ } -+ -+ // Generic constrained dispatch -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task CallGenericConstrained(T obj) where T : IAsyncService -+ { -+ return await obj.GetValueAsync(); -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs -new file mode 100644 -index 00000000000..28ddb079495 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs -@@ -0,0 +1,23 @@ -+// Test: Async method without yields (no suspension point) -+// When a runtime-async method never actually awaits, crossgen2 may -+// omit the resumption stub. This tests that edge case. -+using System; -+using System.Runtime.CompilerServices; -+using System.Threading.Tasks; -+ -+public static class AsyncNoYield -+{ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task AsyncButNoAwait() -+ { -+ return 42; -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task AsyncWithConditionalAwait(bool doAwait) -+ { -+ if (doAwait) -+ await Task.Yield(); -+ return 1; -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs -new file mode 100644 -index 00000000000..29a8fc879ec ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs -@@ -0,0 +1,26 @@ -+// Test: Async method that captures GC refs across await -+// This forces the compiler to emit a ContinuationLayout fixup. -+using System; -+using System.Runtime.CompilerServices; -+using System.Threading.Tasks; -+ -+public static class AsyncWithContinuation -+{ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task CaptureObjectAcrossAwait() -+ { -+ object o = new object(); -+ string s = "hello"; -+ await Task.Yield(); -+ return s + o.GetHashCode(); -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task CaptureMultipleRefsAcrossAwait() -+ { -+ int[] arr = new int[] { 1, 2, 3 }; -+ string text = "world"; -+ await Task.Yield(); -+ return arr[0] + text.Length; -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs -new file mode 100644 -index 00000000000..c6137cfc309 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs -@@ -0,0 +1,36 @@ -+// Test: Basic async method emission in R2R -+// Validates that runtime-async methods produce [ASYNC] variant entries and -+// resumption stubs in the R2R image. -+using System; -+using System.Runtime.CompilerServices; -+using System.Threading.Tasks; -+ -+public static class BasicAsyncEmission -+{ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task SimpleAsyncMethod() -+ { -+ await Task.Yield(); -+ return 42; -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async Task AsyncVoidReturn() -+ { -+ await Task.Yield(); -+ } -+ -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static async ValueTask ValueTaskMethod() -+ { -+ await Task.Yield(); -+ return "hello"; -+ } -+ -+ // Non-async method that returns Task (no await) — should NOT get async variant -+ [MethodImpl(MethodImplOptions.NoInlining)] -+ public static Task SyncTaskReturning() -+ { -+ return Task.FromResult(1); -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs -new file mode 100644 -index 00000000000..bb10453f70b ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs -@@ -0,0 +1,28 @@ -+// Dependency library for async cross-module tests. -+// Contains runtime-async methods that should be inlineable. -+using System; -+using System.Runtime.CompilerServices; -+using System.Threading.Tasks; -+ -+public static class AsyncDepLib -+{ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static async Task GetValueAsync() -+ { -+ await Task.Yield(); -+ return 42; -+ } -+ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static async Task GetStringAsync() -+ { -+ await Task.Yield(); -+ return "async_hello"; -+ } -+ -+ [MethodImpl(MethodImplOptions.AggressiveInlining)] -+ public static int GetValueSync() -+ { -+ return 99; -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs -new file mode 100644 -index 00000000000..e481c3bcefd ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs -@@ -0,0 +1,10 @@ -+// Licensed to the .NET Foundation under one or more agreements. -+// The .NET Foundation licenses this file to you under the MIT license. -+ -+namespace System.Runtime.CompilerServices; -+ -+[AttributeUsage(AttributeTargets.Method)] -+internal class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribute -+{ -+ public bool RuntimeAsync { get; } = runtimeAsync; -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs -new file mode 100644 -index 00000000000..6aa4f9d3479 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs -@@ -0,0 +1,162 @@ -+// Licensed to the .NET Foundation under one or more agreements. -+// The .NET Foundation licenses this file to you under the MIT license. -+ -+using System; -+using System.Collections.Generic; -+using System.Diagnostics; -+using System.IO; -+using System.Linq; -+using System.Text; -+ -+namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; -+ -+/// -+/// Result of a crossgen2 compilation step. -+/// -+internal sealed record R2RCompilationResult( -+ string OutputPath, -+ int ExitCode, -+ string StandardOutput, -+ string StandardError) -+{ -+ public bool Success => ExitCode == 0; -+} -+ -+/// -+/// Options for a single crossgen2 compilation step. -+/// -+internal sealed class R2RCompilationOptions -+{ -+ public required string InputPath { get; init; } -+ public required string OutputPath { get; init; } -+ public List ReferencePaths { get; init; } = new(); -+ public List ExtraArgs { get; init; } = new(); -+ public bool Composite { get; init; } -+ public List? CompositeInputPaths { get; init; } -+ public List? InputBubbleRefs { get; init; } -+} -+ -+/// -+/// Invokes crossgen2 out-of-process to produce R2R images. -+/// -+internal sealed class R2RDriver -+{ -+ private readonly string _crossgen2Dir; -+ -+ public R2RDriver() -+ { -+ _crossgen2Dir = TestPaths.Crossgen2Dir; -+ -+ if (!File.Exists(TestPaths.Crossgen2Dll)) -+ throw new FileNotFoundException($"crossgen2.dll not found at {TestPaths.Crossgen2Dll}"); -+ } -+ -+ /// -+ /// Runs crossgen2 on a single assembly. -+ /// -+ public R2RCompilationResult Compile(R2RCompilationOptions options) -+ { -+ var args = new List(); -+ -+ if (options.Composite) -+ { -+ args.Add("--composite"); -+ if (options.CompositeInputPaths is not null) -+ { -+ foreach (string input in options.CompositeInputPaths) -+ args.Add(input); -+ } -+ } -+ else -+ { -+ args.Add(options.InputPath); -+ } -+ -+ args.Add("-o"); -+ args.Add(options.OutputPath); -+ -+ foreach (string refPath in options.ReferencePaths) -+ { -+ args.Add("-r"); -+ args.Add(refPath); -+ } -+ -+ if (options.InputBubbleRefs is not null) -+ { -+ foreach (string bubbleRef in options.InputBubbleRefs) -+ { -+ args.Add("--inputbubbleref"); -+ args.Add(bubbleRef); -+ } -+ } -+ -+ args.AddRange(options.ExtraArgs); -+ -+ return RunCrossgen2(args); -+ } -+ -+ /// -+ /// Crossgen2 a dependency assembly (simple single-assembly R2R). -+ /// -+ public R2RCompilationResult CompileDependency(string inputPath, string outputPath, IEnumerable referencePaths) -+ { -+ return Compile(new R2RCompilationOptions -+ { -+ InputPath = inputPath, -+ OutputPath = outputPath, -+ ReferencePaths = referencePaths.ToList() -+ }); -+ } -+ -+ private R2RCompilationResult RunCrossgen2(List crossgen2Args) -+ { -+ // Use dotnet exec to invoke crossgen2.dll -+ string dotnetHost = TestPaths.DotNetHost; -+ string crossgen2Dll = TestPaths.Crossgen2Dll; -+ -+ var allArgs = new List { "exec", crossgen2Dll }; -+ allArgs.AddRange(crossgen2Args); -+ -+ string argsString = string.Join(" ", allArgs.Select(QuoteIfNeeded)); -+ -+ var psi = new ProcessStartInfo -+ { -+ FileName = dotnetHost, -+ Arguments = argsString, -+ RedirectStandardOutput = true, -+ RedirectStandardError = true, -+ UseShellExecute = false, -+ CreateNoWindow = true, -+ }; -+ -+ // Strip environment variables that interfere with crossgen2 -+ string[] envVarsToStrip = { "DOTNET_GCName", "DOTNET_GCStress", "DOTNET_HeapVerify", "DOTNET_ReadyToRun" }; -+ foreach (string envVar in envVarsToStrip) -+ { -+ psi.Environment[envVar] = null; -+ } -+ -+ using var process = Process.Start(psi)!; -+ string stdout = process.StandardOutput.ReadToEnd(); -+ string stderr = process.StandardError.ReadToEnd(); -+ process.WaitForExit(); -+ -+ string outputPath = crossgen2Args -+ .SkipWhile(a => a != "-o") -+ .Skip(1) -+ .FirstOrDefault() ?? "unknown"; -+ -+ return new R2RCompilationResult( -+ outputPath, -+ process.ExitCode, -+ stdout, -+ stderr); -+ } -+ -+ private static string QuoteIfNeeded(string arg) -+ { -+ if (arg.Contains(' ') || arg.Contains('"')) -+ return $"\"{arg.Replace("\"", "\\\"")}\""; -+ return arg; -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs -new file mode 100644 -index 00000000000..e0565ad1f07 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs -@@ -0,0 +1,318 @@ -+// Licensed to the .NET Foundation under one or more agreements. -+// The .NET Foundation licenses this file to you under the MIT license. -+ -+using System; -+using System.Collections.Generic; -+using System.IO; -+using System.Linq; -+using System.Reflection.Metadata; -+using System.Reflection.PortableExecutable; -+using ILCompiler.Reflection.ReadyToRun; -+using Internal.ReadyToRunConstants; -+using Xunit; -+ -+namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; -+ -+/// -+/// Parsed expectations from a test case's assembly-level and method-level attributes. -+/// -+internal sealed class R2RExpectations -+{ -+ public List ExpectedManifestRefs { get; } = new(); -+ public List ExpectedInlinedMethods { get; } = new(); -+ public bool CompositeMode { get; set; } -+ public List Crossgen2Options { get; } = new(); -+ /// -+ /// Roslyn feature flags for the main assembly (e.g. runtime-async=on). -+ /// -+ public List> Features { get; } = new(); -+ /// -+ /// Method names expected to have [ASYNC] variant entries in the R2R image. -+ /// -+ public List ExpectedAsyncVariantMethods { get; } = new(); -+ /// -+ /// Method names expected to have [RESUME] (resumption stub) entries. -+ /// -+ public List ExpectedResumptionStubs { get; } = new(); -+ /// -+ /// If true, expect at least one ContinuationLayout fixup in the image. -+ /// -+ public bool ExpectContinuationLayout { get; set; } -+ /// -+ /// If true, expect at least one ResumptionStubEntryPoint fixup in the image. -+ /// -+ public bool ExpectResumptionStubFixup { get; set; } -+ /// -+ /// Fixup kinds that must be present somewhere in the image. -+ /// -+ public List ExpectedFixupKinds { get; } = new(); -+} -+ -+internal sealed record ExpectedInlinedMethod(string MethodName); -+ -+/// -+/// Validates R2R images against test expectations using ReadyToRunReader. -+/// -+internal sealed class R2RResultChecker -+{ -+ /// -+ /// Validates the main R2R image against expectations. -+ /// -+ public void Check(string r2rImagePath, R2RExpectations expectations) -+ { -+ Assert.True(File.Exists(r2rImagePath), $"R2R image not found: {r2rImagePath}"); -+ -+ using var fileStream = File.OpenRead(r2rImagePath); -+ using var peReader = new PEReader(fileStream); -+ -+ Assert.True(ReadyToRunReader.IsReadyToRunImage(peReader), -+ $"'{Path.GetFileName(r2rImagePath)}' is not a valid R2R image"); -+ -+ var reader = new ReadyToRunReader(new SimpleAssemblyResolver(), r2rImagePath); -+ -+ CheckManifestRefs(reader, expectations, r2rImagePath); -+ CheckInlinedMethods(reader, expectations, r2rImagePath); -+ CheckAsyncVariantMethods(reader, expectations, r2rImagePath); -+ CheckResumptionStubs(reader, expectations, r2rImagePath); -+ CheckFixupKinds(reader, expectations, r2rImagePath); -+ } -+ -+ private static void CheckManifestRefs(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) -+ { -+ if (expectations.ExpectedManifestRefs.Count == 0) -+ return; -+ -+ // Get all assembly references (both MSIL and manifest) -+ var allRefs = new HashSet(StringComparer.OrdinalIgnoreCase); -+ -+ // Read MSIL AssemblyRef table -+ var globalMetadata = reader.GetGlobalMetadata(); -+ var mdReader = globalMetadata.MetadataReader; -+ foreach (var handle in mdReader.AssemblyReferences) -+ { -+ var assemblyRef = mdReader.GetAssemblyReference(handle); -+ string name = mdReader.GetString(assemblyRef.Name); -+ allRefs.Add(name); -+ } -+ -+ // Read manifest references (extra refs beyond MSIL table) -+ foreach (var kvp in reader.ManifestReferenceAssemblies) -+ { -+ allRefs.Add(kvp.Key); -+ } -+ -+ foreach (string expected in expectations.ExpectedManifestRefs) -+ { -+ Assert.True(allRefs.Contains(expected), -+ $"Expected assembly reference '{expected}' not found in R2R image '{Path.GetFileName(imagePath)}'. " + -+ $"Found: [{string.Join(", ", allRefs.OrderBy(s => s))}]"); -+ } -+ } -+ -+ private static void CheckInlinedMethods(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) -+ { -+ if (expectations.ExpectedInlinedMethods.Count == 0) -+ return; -+ -+ var checkIlBodySignatures = new HashSet(StringComparer.OrdinalIgnoreCase); -+ var formattingOptions = new SignatureFormattingOptions(); -+ var allFixupSummary = new List(); -+ -+ void CollectFixups(ReadyToRunMethod method) -+ { -+ foreach (var cell in method.Fixups) -+ { -+ if (cell.Signature is null) -+ continue; -+ -+ string sigText = cell.Signature.ToString(formattingOptions); -+ allFixupSummary.Add($"[{cell.Signature.FixupKind}] {sigText}"); -+ -+ if (cell.Signature.FixupKind is ReadyToRunFixupKind.Check_IL_Body or ReadyToRunFixupKind.Verify_IL_Body) -+ { -+ checkIlBodySignatures.Add(sigText); -+ } -+ } -+ } -+ -+ foreach (var assembly in reader.ReadyToRunAssemblies) -+ { -+ foreach (var method in assembly.Methods) -+ CollectFixups(method); -+ } -+ -+ foreach (var instanceMethod in reader.InstanceMethods) -+ CollectFixups(instanceMethod.Method); -+ -+ foreach (var expected in expectations.ExpectedInlinedMethods) -+ { -+ bool found = checkIlBodySignatures.Any(f => -+ f.Contains(expected.MethodName, StringComparison.OrdinalIgnoreCase)); -+ -+ Assert.True(found, -+ $"Expected CHECK_IL_BODY fixup for '{expected.MethodName}' not found in '{Path.GetFileName(imagePath)}'. " + -+ $"CHECK_IL_BODY fixups: [{string.Join(", ", checkIlBodySignatures)}]. " + -+ $"All fixups: [{string.Join("; ", allFixupSummary)}]"); -+ } -+ } -+ -+ private static List GetAllMethods(ReadyToRunReader reader) -+ { -+ var methods = new List(); -+ foreach (var assembly in reader.ReadyToRunAssemblies) -+ methods.AddRange(assembly.Methods); -+ foreach (var instanceMethod in reader.InstanceMethods) -+ methods.Add(instanceMethod.Method); -+ -+ return methods; -+ } -+ -+ private static void CheckAsyncVariantMethods(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) -+ { -+ if (expectations.ExpectedAsyncVariantMethods.Count == 0) -+ return; -+ -+ var allMethods = GetAllMethods(reader); -+ var asyncMethods = allMethods -+ .Where(m => m.SignatureString.Contains("[ASYNC]", StringComparison.OrdinalIgnoreCase)) -+ .Select(m => m.SignatureString) -+ .ToList(); -+ -+ foreach (string expected in expectations.ExpectedAsyncVariantMethods) -+ { -+ bool found = asyncMethods.Any(sig => -+ sig.Contains(expected, StringComparison.OrdinalIgnoreCase)); -+ -+ Assert.True(found, -+ $"Expected [ASYNC] variant for '{expected}' not found in '{Path.GetFileName(imagePath)}'. " + -+ $"Async methods: [{string.Join(", ", asyncMethods)}]. " + -+ $"All methods: [{string.Join(", ", allMethods.Select(m => m.SignatureString).Take(30))}]"); -+ } -+ } -+ -+ private static void CheckResumptionStubs(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) -+ { -+ if (expectations.ExpectedResumptionStubs.Count == 0 && !expectations.ExpectResumptionStubFixup) -+ return; -+ -+ var allMethods = GetAllMethods(reader); -+ var resumeMethods = allMethods -+ .Where(m => m.SignatureString.Contains("[RESUME]", StringComparison.OrdinalIgnoreCase)) -+ .Select(m => m.SignatureString) -+ .ToList(); -+ -+ foreach (string expected in expectations.ExpectedResumptionStubs) -+ { -+ bool found = resumeMethods.Any(sig => -+ sig.Contains(expected, StringComparison.OrdinalIgnoreCase)); -+ -+ Assert.True(found, -+ $"Expected [RESUME] stub for '{expected}' not found in '{Path.GetFileName(imagePath)}'. " + -+ $"Resume methods: [{string.Join(", ", resumeMethods)}]. " + -+ $"All methods: [{string.Join(", ", allMethods.Select(m => m.SignatureString).Take(30))}]"); -+ } -+ -+ if (expectations.ExpectResumptionStubFixup) -+ { -+ var formattingOptions = new SignatureFormattingOptions(); -+ bool hasResumptionFixup = allMethods.Any(m => -+ m.Fixups.Any(c => -+ c.Signature?.FixupKind == ReadyToRunFixupKind.ResumptionStubEntryPoint)); -+ -+ Assert.True(hasResumptionFixup, -+ $"Expected ResumptionStubEntryPoint fixup not found in '{Path.GetFileName(imagePath)}'."); -+ } -+ } -+ -+ private static void CheckFixupKinds(ReadyToRunReader reader, R2RExpectations expectations, string imagePath) -+ { -+ if (expectations.ExpectedFixupKinds.Count == 0 && !expectations.ExpectContinuationLayout) -+ return; -+ -+ var allMethods = GetAllMethods(reader); -+ var presentKinds = new HashSet(); -+ foreach (var method in allMethods) -+ { -+ if (method.Fixups is null) -+ continue; -+ foreach (var cell in method.Fixups) -+ { -+ if (cell.Signature is not null) -+ presentKinds.Add(cell.Signature.FixupKind); -+ } -+ } -+ -+ if (expectations.ExpectContinuationLayout) -+ { -+ Assert.True(presentKinds.Contains(ReadyToRunFixupKind.ContinuationLayout), -+ $"Expected ContinuationLayout fixup not found in '{Path.GetFileName(imagePath)}'. " + -+ $"Present fixup kinds: [{string.Join(", ", presentKinds)}]"); -+ } -+ -+ foreach (var expectedKind in expectations.ExpectedFixupKinds) -+ { -+ Assert.True(presentKinds.Contains(expectedKind), -+ $"Expected fixup kind '{expectedKind}' not found in '{Path.GetFileName(imagePath)}'. " + -+ $"Present fixup kinds: [{string.Join(", ", presentKinds)}]"); -+ } -+ } -+} -+ -+/// -+/// Simple assembly resolver that looks in the same directory as the input image. -+/// -+internal sealed class SimpleAssemblyResolver : IAssemblyResolver -+{ -+ private readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase); -+ -+ public IAssemblyMetadata? FindAssembly(MetadataReader metadataReader, AssemblyReferenceHandle assemblyReferenceHandle, string parentFile) -+ { -+ var assemblyRef = metadataReader.GetAssemblyReference(assemblyReferenceHandle); -+ string name = metadataReader.GetString(assemblyRef.Name); -+ return FindAssembly(name, parentFile); -+ } -+ -+ public IAssemblyMetadata? FindAssembly(string simpleName, string parentFile) -+ { -+ string? dir = Path.GetDirectoryName(parentFile); -+ if (dir is null) -+ return null; -+ -+ string candidate = Path.Combine(dir, simpleName + ".dll"); -+ if (!File.Exists(candidate)) -+ { -+ // Try in runtime pack -+ candidate = Path.Combine(TestPaths.RuntimePackDir, simpleName + ".dll"); -+ } -+ -+ if (!File.Exists(candidate)) -+ return null; -+ -+ return new SimpleAssemblyMetadata(candidate); -+ } -+} -+ -+/// -+/// Simple assembly metadata wrapper. -+/// -+internal sealed class SimpleAssemblyMetadata : IAssemblyMetadata, IDisposable -+{ -+ private readonly FileStream _stream; -+ private readonly PEReader _peReader; -+ -+ public SimpleAssemblyMetadata(string path) -+ { -+ _stream = File.OpenRead(path); -+ _peReader = new PEReader(_stream); -+ } -+ -+ public PEReader ImageReader => _peReader; -+ -+ public MetadataReader MetadataReader => _peReader.GetMetadataReader(); -+ -+ public void Dispose() -+ { -+ _peReader.Dispose(); -+ _stream.Dispose(); -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs -new file mode 100644 -index 00000000000..c92c52d16c5 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs -@@ -0,0 +1,131 @@ -+// Licensed to the .NET Foundation under one or more agreements. -+// The .NET Foundation licenses this file to you under the MIT license. -+ -+using System; -+using System.Collections.Generic; -+using System.IO; -+using System.Linq; -+using System.Reflection; -+using System.Reflection.Metadata; -+using Microsoft.CodeAnalysis; -+using Microsoft.CodeAnalysis.CSharp; -+using Microsoft.CodeAnalysis.Emit; -+ -+namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; -+ -+/// -+/// Compiles C# source code into assemblies using Roslyn at test time. -+/// -+internal sealed class R2RTestCaseCompiler -+{ -+ private readonly string _outputDir; -+ private readonly List _frameworkReferences; -+ -+ public R2RTestCaseCompiler(string outputDir) -+ { -+ _outputDir = outputDir; -+ _frameworkReferences = new List(); -+ -+ // Add reference assemblies from the ref pack (needed for Roslyn compilation) -+ string refPackDir = TestPaths.RefPackDir; -+ if (Directory.Exists(refPackDir)) -+ { -+ foreach (string refPath in Directory.EnumerateFiles(refPackDir, "*.dll")) -+ { -+ _frameworkReferences.Add(MetadataReference.CreateFromFile(refPath)); -+ } -+ } -+ else -+ { -+ // Fallback to runtime pack implementation assemblies -+ foreach (string refPath in TestPaths.GetFrameworkReferencePaths()) -+ { -+ _frameworkReferences.Add(MetadataReference.CreateFromFile(refPath)); -+ } -+ } -+ } -+ -+ /// -+ /// Compiles a single assembly from source files. -+ /// -+ /// Name of the output assembly (without .dll extension). -+ /// C# source code strings. -+ /// Paths to additional assembly references. -+ /// Library or ConsoleApplication. -+ /// Additional preprocessor defines. -+ /// Roslyn feature flags (e.g. "runtime-async=on"). -+ /// Path to the compiled assembly. -+ public string CompileAssembly( -+ string assemblyName, -+ IEnumerable sources, -+ IEnumerable? additionalReferences = null, -+ OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, -+ IEnumerable? additionalDefines = null, -+ IEnumerable>? features = null) -+ { -+ var parseOptions = new CSharpParseOptions( -+ LanguageVersion.Latest, -+ preprocessorSymbols: additionalDefines); -+ -+ if (features is not null) -+ parseOptions = parseOptions.WithFeatures(features); -+ -+ var syntaxTrees = sources.Select(src => -+ CSharpSyntaxTree.ParseText(src, parseOptions)); -+ -+ var references = new List(_frameworkReferences); -+ if (additionalReferences is not null) -+ { -+ foreach (string refPath in additionalReferences) -+ { -+ references.Add(MetadataReference.CreateFromFile(refPath)); -+ } -+ } -+ -+ var compilation = CSharpCompilation.Create( -+ assemblyName, -+ syntaxTrees, -+ references, -+ new CSharpCompilationOptions(outputKind) -+ .WithOptimizationLevel(OptimizationLevel.Release) -+ .WithAllowUnsafe(true) -+ .WithNullableContextOptions(NullableContextOptions.Enable)); -+ -+ string outputPath = Path.Combine(_outputDir, assemblyName + ".dll"); -+ EmitResult result = compilation.Emit(outputPath); -+ -+ if (!result.Success) -+ { -+ var errors = result.Diagnostics -+ .Where(d => d.Severity == DiagnosticSeverity.Error) -+ .Select(d => d.ToString()); -+ throw new InvalidOperationException( -+ $"Compilation of '{assemblyName}' failed:\n{string.Join("\n", errors)}"); -+ } -+ -+ return outputPath; -+ } -+ -+ /// -+ /// Reads an embedded resource from the test assembly. -+ /// -+ public static string ReadEmbeddedSource(string resourceName) -+ { -+ var assembly = Assembly.GetExecutingAssembly(); -+ using Stream? stream = assembly.GetManifestResourceStream(resourceName); -+ if (stream is null) -+ { -+ // Try with different path separator -+ string altName = resourceName.Replace('/', '\\'); -+ using Stream? altStream = assembly.GetManifestResourceStream(altName); -+ if (altStream is null) -+ throw new FileNotFoundException($"Embedded resource not found: '{resourceName}'. Available: {string.Join(", ", assembly.GetManifestResourceNames())}"); -+ -+ using var altReader = new StreamReader(altStream); -+ return altReader.ReadToEnd(); -+ } -+ -+ using var reader = new StreamReader(stream); -+ return reader.ReadToEnd(); -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs -new file mode 100644 -index 00000000000..22131fe08ac ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs -@@ -0,0 +1,221 @@ -+// Licensed to the .NET Foundation under one or more agreements. -+// The .NET Foundation licenses this file to you under the MIT license. -+ -+using System; -+using System.Collections.Generic; -+using System.IO; -+using System.Linq; -+using ILCompiler.ReadyToRun.Tests.Expectations; -+using Xunit; -+ -+namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; -+ -+/// -+/// Describes a test case: a main source file with its dependencies and expectations. -+/// -+internal sealed class R2RTestCase -+{ -+ public required string Name { get; init; } -+ public required string MainSourceResourceName { get; init; } -+ /// -+ /// Additional source files to compile with the main assembly (e.g. shared attribute files). -+ /// -+ public string[]? MainExtraSourceResourceNames { get; init; } -+ public required List Dependencies { get; init; } -+ public required R2RExpectations Expectations { get; init; } -+} -+ -+/// -+/// Describes a dependency assembly for a test case. -+/// -+internal sealed class DependencyInfo -+{ -+ public required string AssemblyName { get; init; } -+ public required string[] SourceResourceNames { get; init; } -+ public bool Crossgen { get; init; } -+ public List CrossgenOptions { get; init; } = new(); -+ public List AdditionalReferences { get; init; } = new(); -+ /// -+ /// Roslyn feature flags for this dependency (e.g. runtime-async=on). -+ /// -+ public List> Features { get; init; } = new(); -+} -+ -+/// -+/// Orchestrates the full R2R test pipeline: compile → crossgen2 → validate. -+/// -+internal sealed class R2RTestRunner -+{ -+ /// -+ /// Runs a test case end-to-end. -+ /// -+ public void Run(R2RTestCase testCase) -+ { -+ string tempDir = Path.Combine(Path.GetTempPath(), "R2RTests", testCase.Name, Guid.NewGuid().ToString("N")[..8]); -+ string ilDir = Path.Combine(tempDir, "il"); -+ string r2rDir = Path.Combine(tempDir, "r2r"); -+ -+ try -+ { -+ Directory.CreateDirectory(ilDir); -+ Directory.CreateDirectory(r2rDir); -+ -+ // Step 1: Compile all dependencies with Roslyn -+ var compiler = new R2RTestCaseCompiler(ilDir); -+ var compiledDeps = new List<(DependencyInfo Dep, string IlPath)>(); -+ -+ foreach (var dep in testCase.Dependencies) -+ { -+ var sources = dep.SourceResourceNames -+ .Select(R2RTestCaseCompiler.ReadEmbeddedSource) -+ .ToList(); -+ -+ var refs = dep.AdditionalReferences -+ .Select(r => compiledDeps.First(d => d.Dep.AssemblyName == r).IlPath) -+ .ToList(); -+ -+ string ilPath = compiler.CompileAssembly(dep.AssemblyName, sources, refs, -+ features: dep.Features.Count > 0 ? dep.Features : null); -+ compiledDeps.Add((dep, ilPath)); -+ } -+ -+ // Step 2: Compile main assembly with Roslyn -+ var mainSources = new List -+ { -+ R2RTestCaseCompiler.ReadEmbeddedSource(testCase.MainSourceResourceName) -+ }; -+ if (testCase.MainExtraSourceResourceNames is not null) -+ { -+ foreach (string extra in testCase.MainExtraSourceResourceNames) -+ mainSources.Add(R2RTestCaseCompiler.ReadEmbeddedSource(extra)); -+ } -+ -+ var mainRefs = compiledDeps.Select(d => d.IlPath).ToList(); -+ string mainIlPath = compiler.CompileAssembly(testCase.Name, mainSources, mainRefs, -+ outputKind: Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary, -+ features: testCase.Expectations.Features.Count > 0 ? testCase.Expectations.Features : null); -+ -+ // Step 3: Crossgen2 dependencies -+ var driver = new R2RDriver(); -+ var allRefPaths = BuildReferencePaths(ilDir); -+ -+ foreach (var (dep, ilPath) in compiledDeps) -+ { -+ if (!dep.Crossgen) -+ continue; -+ -+ string r2rPath = Path.Combine(r2rDir, Path.GetFileName(ilPath)); -+ var result = driver.Compile(new R2RCompilationOptions -+ { -+ InputPath = ilPath, -+ OutputPath = r2rPath, -+ ReferencePaths = allRefPaths, -+ ExtraArgs = dep.CrossgenOptions, -+ }); -+ -+ Assert.True(result.Success, -+ $"crossgen2 failed for dependency '{dep.AssemblyName}':\n{result.StandardError}\n{result.StandardOutput}"); -+ } -+ -+ // Step 4: Crossgen2 main assembly -+ string mainR2RPath = Path.Combine(r2rDir, Path.GetFileName(mainIlPath)); -+ -+ if (testCase.Expectations.CompositeMode) -+ { -+ RunCompositeCompilation(testCase, driver, ilDir, r2rDir, mainIlPath, mainR2RPath, allRefPaths, compiledDeps); -+ } -+ else -+ { -+ RunSingleCompilation(testCase, driver, mainIlPath, mainR2RPath, allRefPaths); -+ } -+ -+ // Step 5: Validate R2R output -+ var checker = new R2RResultChecker(); -+ checker.Check(mainR2RPath, testCase.Expectations); -+ } -+ finally -+ { -+ // Keep temp directory for debugging if KEEP_R2R_TESTS env var is set -+ if (Environment.GetEnvironmentVariable("KEEP_R2R_TESTS") is null) -+ { -+ try { Directory.Delete(tempDir, true); } -+ catch { /* best effort */ } -+ } -+ } -+ } -+ -+ private static void RunSingleCompilation( -+ R2RTestCase testCase, -+ R2RDriver driver, -+ string mainIlPath, -+ string mainR2RPath, -+ List allRefPaths) -+ { -+ var options = new R2RCompilationOptions -+ { -+ InputPath = mainIlPath, -+ OutputPath = mainR2RPath, -+ ReferencePaths = allRefPaths, -+ ExtraArgs = testCase.Expectations.Crossgen2Options.ToList(), -+ }; -+ -+ var result = driver.Compile(options); -+ Assert.True(result.Success, -+ $"crossgen2 failed for main assembly '{testCase.Name}':\n{result.StandardError}\n{result.StandardOutput}"); -+ } -+ -+ private static void RunCompositeCompilation( -+ R2RTestCase testCase, -+ R2RDriver driver, -+ string ilDir, -+ string r2rDir, -+ string mainIlPath, -+ string mainR2RPath, -+ List allRefPaths, -+ List<(DependencyInfo Dep, string IlPath)> compiledDeps) -+ { -+ var compositeInputs = new List { mainIlPath }; -+ foreach (var (dep, ilPath) in compiledDeps) -+ { -+ if (dep.Crossgen) -+ compositeInputs.Add(ilPath); -+ } -+ -+ var options = new R2RCompilationOptions -+ { -+ InputPath = mainIlPath, -+ OutputPath = mainR2RPath, -+ ReferencePaths = allRefPaths, -+ Composite = true, -+ CompositeInputPaths = compositeInputs, -+ ExtraArgs = testCase.Expectations.Crossgen2Options.ToList(), -+ }; -+ -+ var result = driver.Compile(options); -+ Assert.True(result.Success, -+ $"crossgen2 composite compilation failed for '{testCase.Name}':\n{result.StandardError}\n{result.StandardOutput}"); -+ } -+ -+ private static List BuildReferencePaths(string ilDir) -+ { -+ var paths = new List(); -+ -+ // Add all compiled IL assemblies as references -+ paths.Add(Path.Combine(ilDir, "*.dll")); -+ -+ // Add framework references (managed assemblies) -+ paths.Add(Path.Combine(TestPaths.RuntimePackDir, "*.dll")); -+ -+ // System.Private.CoreLib is in the native directory, not lib -+ string runtimePackDir = TestPaths.RuntimePackDir; -+ string nativeDir = Path.GetFullPath(Path.Combine(runtimePackDir, "..", "..", "native")); -+ if (Directory.Exists(nativeDir)) -+ { -+ string spcl = Path.Combine(nativeDir, "System.Private.CoreLib.dll"); -+ if (File.Exists(spcl)) -+ paths.Add(spcl); -+ } -+ -+ return paths; -+ } -+} -diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs -new file mode 100644 -index 00000000000..2524ae4fd86 ---- /dev/null -+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs -@@ -0,0 +1,167 @@ -+// Licensed to the .NET Foundation under one or more agreements. -+// The .NET Foundation licenses this file to you under the MIT license. -+ -+using System; -+using System.Collections.Generic; -+using System.IO; -+using System.Runtime.InteropServices; -+using System.Text.RegularExpressions; -+ -+namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; -+ -+/// -+/// Provides paths to build artifacts needed by the test infrastructure. -+/// All paths come from RuntimeHostConfigurationOption items in the csproj. -+/// -+internal static class TestPaths -+{ -+ private static string GetRequiredConfig(string key) -+ { -+ return AppContext.GetData(key) as string -+ ?? throw new InvalidOperationException($"Missing RuntimeHostConfigurationOption '{key}'. Was the project built with the correct properties?"); -+ } -+ -+ /// -+ /// Path to the crossgen2 output directory (contains crossgen2.dll and clrjit). -+ /// e.g. artifacts/bin/coreclr/linux.x64.Checked/crossgen2/ -+ /// Falls back to Checked or Release if Debug path doesn't exist. -+ /// -+ public static string Crossgen2Dir -+ { -+ get -+ { -+ string dir = GetRequiredConfig("R2RTest.Crossgen2Dir"); -+ if (!Directory.Exists(dir)) -+ { -+ // Try Checked and Release fallbacks since crossgen2 may be built in a different config -+ foreach (string fallbackConfig in new[] { "Checked", "Release", "Debug" }) -+ { -+ string fallback = Regex.Replace( -+ dir, @"\.(Debug|Release|Checked)[/\\]", $".{fallbackConfig}/"); -+ if (Directory.Exists(fallback)) -+ return fallback; -+ } -+ } -+ -+ return dir; -+ } -+ } -+ -+ /// -+ /// Path to the crossgen2.dll managed assembly. -+ /// -+ public static string Crossgen2Dll => Path.Combine(Crossgen2Dir, "crossgen2.dll"); -+ -+ /// -+ /// Path to the runtime pack managed assemblies directory. -+ /// e.g. artifacts/bin/microsoft.netcore.app.runtime.linux-x64/Release/runtimes/linux-x64/lib/net11.0/ -+ /// Falls back to Release if Debug path doesn't exist (libs are typically built Release). -+ /// -+ public static string RuntimePackDir -+ { -+ get -+ { -+ string dir = GetRequiredConfig("R2RTest.RuntimePackDir"); -+ if (!Directory.Exists(dir) && dir.Contains("Debug")) -+ { -+ string releaseFallback = dir.Replace("Debug", "Release"); -+ if (Directory.Exists(releaseFallback)) -+ return releaseFallback; -+ } -+ -+ return dir; -+ } -+ } -+ -+ /// -+ /// Path to the CoreCLR artifacts directory (contains native bits like corerun). -+ /// e.g. artifacts/bin/coreclr/linux.x64.Checked/ -+ /// Falls back to Checked or Release if Debug path doesn't exist. -+ /// -+ public static string CoreCLRArtifactsDir -+ { -+ get -+ { -+ string dir = GetRequiredConfig("R2RTest.CoreCLRArtifactsDir"); -+ if (!Directory.Exists(dir)) -+ { -+ foreach (string fallbackConfig in new[] { "Checked", "Release", "Debug" }) -+ { -+ string fallback = Regex.Replace( -+ dir, @"\.(Debug|Release|Checked)(/|\\|$)", $".{fallbackConfig}$2"); -+ if (Directory.Exists(fallback)) -+ return fallback; -+ } -+ } -+ -+ return dir; -+ } -+ } -+ -+ public static string TargetArchitecture => GetRequiredConfig("R2RTest.TargetArchitecture"); -+ public static string TargetOS => GetRequiredConfig("R2RTest.TargetOS"); -+ public static string Configuration => GetRequiredConfig("R2RTest.Configuration"); -+ -+ /// -+ /// Path to the reference assembly pack (for Roslyn compilation). -+ /// e.g. artifacts/bin/microsoft.netcore.app.ref/ref/net11.0/ -+ /// -+ public static string RefPackDir -+ { -+ get -+ { -+ string dir = GetRequiredConfig("R2RTest.RefPackDir"); -+ if (!Directory.Exists(dir)) -+ { -+ // Try the artifacts/bin/ref/net* fallback -+ string artifactsBin = Path.GetFullPath(Path.Combine(CoreCLRArtifactsDir, "..", "..")); -+ string refDir = Path.Combine(artifactsBin, "ref"); -+ if (Directory.Exists(refDir)) -+ { -+ foreach (string subDir in Directory.GetDirectories(refDir, "net*")) -+ { -+ if (File.Exists(Path.Combine(subDir, "System.Runtime.dll"))) -+ return subDir; -+ } -+ } -+ } -+ -+ return dir; -+ } -+ } -+ -+ /// -+ /// Returns the dotnet host executable path suitable for running crossgen2. -+ /// -+ public static string DotNetHost -+ { -+ get -+ { -+ string repoRoot = Path.GetFullPath(Path.Combine(CoreCLRArtifactsDir, "..", "..", "..", "..")); -+ string dotnetDir = Path.Combine(repoRoot, ".dotnet"); -+ string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; -+ string path = Path.Combine(dotnetDir, exe); -+ if (File.Exists(path)) -+ return path; -+ -+ // Fallback to PATH -+ return exe; -+ } -+ } -+ -+ /// -+ /// Returns the target triple string for crossgen2 (e.g. "linux-x64"). -+ /// -+ public static string TargetTriple => $"{TargetOS.ToLowerInvariant()}-{TargetArchitecture.ToLowerInvariant()}"; -+ -+ /// -+ /// Returns all framework reference assembly paths (*.dll in the runtime pack). -+ /// -+ public static IEnumerable GetFrameworkReferencePaths() -+ { -+ if (!Directory.Exists(RuntimePackDir)) -+ throw new DirectoryNotFoundException($"Runtime pack directory not found: {RuntimePackDir}"); -+ -+ return Directory.EnumerateFiles(RuntimePackDir, "*.dll"); -+ } -+} From 61ea4a905a655506cedbcb669baabefb72445625 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:17:47 -0700 Subject: [PATCH 26/74] Remove crossmoduleresolution tests from src/tests/readytorun These tests were superseded by the ILCompiler.ReadyToRun.Tests test suite which compiles and validates R2R images programmatically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../crossmoduleresolution/README.md | 37 --- .../crossmoduleresolution/assemblyB/B.cs | 17 - .../assemblyB/assemblyB.csproj | 8 - .../crossmoduleresolution/assemblyC/C.cs | 115 ------- .../assemblyC/assemblyC.csproj | 12 - .../crossmoduleresolution/assemblyD/D.cs | 30 -- .../assemblyD/assemblyD.csproj | 8 - .../crossmoduleresolution/assemblyE/E.cs | 6 - .../assemblyE/assemblyE.csproj | 9 - .../crossmoduleresolution.targets | 73 ----- .../crossmoduleresolution/main/main.cs | 173 ---------- .../main/main_bubble.csproj | 46 --- .../main/main_crossmodule.csproj | 48 --- .../r2rvalidate/R2RValidate.cs | 303 ------------------ .../r2rvalidate/r2rvalidate.csproj | 14 - .../runcrossgen/RunCrossgen.cs | 210 ------------ .../runcrossgen/runcrossgen.csproj | 16 - 17 files changed, 1125 deletions(-) delete mode 100644 src/tests/readytorun/crossmoduleresolution/README.md delete mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyB/B.cs delete mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyB/assemblyB.csproj delete mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs delete mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj delete mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyD/D.cs delete mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyD/assemblyD.csproj delete mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyE/E.cs delete mode 100644 src/tests/readytorun/crossmoduleresolution/assemblyE/assemblyE.csproj delete mode 100644 src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets delete mode 100644 src/tests/readytorun/crossmoduleresolution/main/main.cs delete mode 100644 src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj delete mode 100644 src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj delete mode 100644 src/tests/readytorun/crossmoduleresolution/r2rvalidate/R2RValidate.cs delete mode 100644 src/tests/readytorun/crossmoduleresolution/r2rvalidate/r2rvalidate.csproj delete mode 100644 src/tests/readytorun/crossmoduleresolution/runcrossgen/RunCrossgen.cs delete mode 100644 src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj diff --git a/src/tests/readytorun/crossmoduleresolution/README.md b/src/tests/readytorun/crossmoduleresolution/README.md deleted file mode 100644 index 8b78a0615680f5..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Cross-Module Resolution Tests - -These tests verify R2R (ReadyToRun) cross-module reference resolution across different -assembly categories and crossgen2 compilation modes. - -## Assembly Graph - -- **Assembly A** (`main/`) — Main test driver, compilation target -- **Assembly B** (`assemblyB/`) — Version bubble (`--inputbubbleref`) -- **Assembly C** (`assemblyC/`) — Cross-module-inlineable only (`--opt-cross-module`) -- **Assembly D** (`assemblyD/`) — External/transitive dependency (not in any set) -- **Assembly E** (`assemblyE/`) — TypeForwarder assembly (forwards to D) - -## Test Modes - -| Variant | Crossgen2 Flags | What It Tests | -|---------|----------------|---------------| -| `main_crossmodule` | `--opt-cross-module:assemblyC` | MutableModule `#:N` and `#D:N` ModuleRef resolution | -| `main_bubble` | `--inputbubbleref:assemblyB.dll` | Version bubble MODULE_ZAPSIG encoding | - -## Test Cases - -Each test method exercises a different cross-module reference scenario: -- TypeRef from version bubble (B) and cross-module-only (C) -- Method calls and field accesses across module boundaries -- Transitive dependencies (C → D) -- Nested types, type forwarders, mixed-origin generics -- Interface dispatch with cross-module interfaces -- Custom ALC loading - -## Building - -These are pri1 tests. Build with `-priority1`: - -```bash -src/tests/build.sh -Test src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj x64 Release -priority1 -``` diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyB/B.cs b/src/tests/readytorun/crossmoduleresolution/assemblyB/B.cs deleted file mode 100644 index 34020f62ebc0ea..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/assemblyB/B.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace AssemblyB -{ - public class BType - { - public int Value => 7; - } - - public class BClass - { - public static int StaticField = 777; - - public static int StaticMethod() => 77; - } -} diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyB/assemblyB.csproj b/src/tests/readytorun/crossmoduleresolution/assemblyB/assemblyB.csproj deleted file mode 100644 index e2440d3c0c8474..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/assemblyB/assemblyB.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - library - - - - - diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs b/src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs deleted file mode 100644 index c3f42b474f31e6..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/assemblyC/C.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace AssemblyC -{ - public class CType - { - public int Value => 3; - } - - public class CClass - { - public static int StaticField = 50; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int UseOwnType() => new CType().Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int UseDType() => new AssemblyD.DType().Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int CallDMethod() => AssemblyD.DClass.StaticMethod(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ReadDField() => AssemblyD.DClass.StaticField; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int UseNestedType() => new AssemblyD.Outer.Inner().GetValue(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string UseForwardedType() => AssemblyD.SomeForwardedType.Name; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int UseGenericWithDType() - { - var list = new List(); - list.Add(new AssemblyD.DType()); - return list[0].Value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int UseCoreLibGeneric() - { - var list = new List { 1, 2, 3 }; - return list.Count; - } - - // --- Async variants (runtime-async thunk targets) --- - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task UseOwnTypeAsync() => new CType().Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task UseDTypeAsync() => new AssemblyD.DType().Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task CallDMethodAsync() => AssemblyD.DClass.StaticMethod(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task ReadDFieldAsync() => AssemblyD.DClass.StaticField; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task UseNestedTypeAsync() => new AssemblyD.Outer.Inner().GetValue(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task UseForwardedTypeAsync() => AssemblyD.SomeForwardedType.Name; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task UseGenericWithDTypeAsync() - { - var list = new List(); - list.Add(new AssemblyD.DType()); - return list[0].Value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task UseCoreLibGenericAsync() - { - var list = new List { 1, 2, 3 }; - return list.Count; - } - - // Task-returning (void-equivalent) async variants - public static int AsyncSideEffect; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task UseOwnTypeAsyncVoid() - { - AsyncSideEffect = new CType().Value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task UseDTypeAsyncVoid() - { - AsyncSideEffect = new AssemblyD.DType().Value; - } - } - - public class CGeneric - { - public T Value { get; set; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetCount() => 1; - } - - public interface ICrossModule - { - int DoWork(); - } -} diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj b/src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj deleted file mode 100644 index 6ea9a5266546ec..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/assemblyC/assemblyC.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - library - $(Features);runtime-async=on - $(NoWarn);CS1998 - - - - - - - diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyD/D.cs b/src/tests/readytorun/crossmoduleresolution/assemblyD/D.cs deleted file mode 100644 index a3869a6d2d1c57..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/assemblyD/D.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace AssemblyD -{ - public class DType - { - public int Value => 42; - } - - public class DClass - { - public static int StaticField = 100; - - public static int StaticMethod() => StaticField + 1; - } - - public class Outer - { - public class Inner - { - public int GetValue() => 99; - } - } - - public class SomeForwardedType - { - public static string Name => "forwarded"; - } -} diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyD/assemblyD.csproj b/src/tests/readytorun/crossmoduleresolution/assemblyD/assemblyD.csproj deleted file mode 100644 index 155932bd710178..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/assemblyD/assemblyD.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - library - - - - - diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyE/E.cs b/src/tests/readytorun/crossmoduleresolution/assemblyE/E.cs deleted file mode 100644 index 56e99beae7a1e6..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/assemblyE/E.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.CompilerServices; - -[assembly: TypeForwardedTo(typeof(AssemblyD.SomeForwardedType))] diff --git a/src/tests/readytorun/crossmoduleresolution/assemblyE/assemblyE.csproj b/src/tests/readytorun/crossmoduleresolution/assemblyE/assemblyE.csproj deleted file mode 100644 index 4de3dc21b44be9..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/assemblyE/assemblyE.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - library - - - - - - diff --git a/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets b/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets deleted file mode 100644 index 56e355cb2e868c..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/crossmoduleresolution.targets +++ /dev/null @@ -1,73 +0,0 @@ - - - - <_RunCrossgenProjectDir>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', 'runcrossgen')) - <_R2RValidateProjectDir>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', 'r2rvalidate')) - - - - - - - <_Crossgen2Path>$(CORE_ROOT)/crossgen2/crossgen2 - <_CoreRunPath>$(CORE_ROOT)/corerun - - - - - <_StepArgs>@(Crossgen2Step->'--assembly %(Identity) --extra-args "%(ExtraArgs)"', ' ') - <_MainRefList>@(Crossgen2MainRef->'%(Identity)', ',') - - - - - <_R2RValArgs>@(R2RExpectRef->'--expect-manifest-ref %(Identity)', ' ') - <_R2RValArgs>$(_R2RValArgs) @(R2RExpectInlined->'--expect-inlined %(Identity)', ' ') - <_R2RValArgs>$(_R2RValArgs) @(R2RExpectAsyncThunks->'--expect-async-thunks %(Identity)', ' ') - - - - - <_ValidateDll>$([MSBuild]::NormalizePath('$(OutputPath)', '..', '..', 'r2rvalidate', 'r2rvalidate', 'r2rvalidate.dll')) - - - - - <_RunCrossgenDll>$(CORE_ROOT)/runcrossgen/runcrossgen.dll - - - - - - - - diff --git a/src/tests/readytorun/crossmoduleresolution/main/main.cs b/src/tests/readytorun/crossmoduleresolution/main/main.cs deleted file mode 100644 index ca81473480837c..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/main/main.cs +++ /dev/null @@ -1,173 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.IO; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.Loader; -using System.Threading.Tasks; - -public static class Assert -{ - public static bool HasAssertFired; - - public static void AreEqual(object actual, object expected) - { - if (!(actual is null && expected is null) && !actual.Equals(expected)) - { - Console.WriteLine("Not equal!"); - Console.WriteLine("actual = " + actual.ToString()); - Console.WriteLine("expected = " + expected.ToString()); - HasAssertFired = true; - } - } -} - -class CrossModuleImpl : AssemblyC.ICrossModule -{ - public int DoWork() => 42; -} - -class Program -{ - // --- Version Bubble Tests (B) --- - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestTypeRef_VersionBubble() => new AssemblyB.BType().Value; - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestMethodCall_VersionBubble() => AssemblyB.BClass.StaticMethod(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestFieldAccess_VersionBubble() => AssemblyB.BClass.StaticField; - - // --- Cross-Module-Only Tests (C → inlined into main) --- - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestTypeRef_CrossModuleOwn() => AssemblyC.CClass.UseOwnType(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestTypeRef_Transitive() => AssemblyC.CClass.UseDType(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestMethodCall_Transitive() => AssemblyC.CClass.CallDMethod(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestFieldAccess_Transitive() => AssemblyC.CClass.ReadDField(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestNestedType_External() => AssemblyC.CClass.UseNestedType(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static string TestTypeForwarder() => AssemblyC.CClass.UseForwardedType(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestGeneric_MixedOrigin() => AssemblyC.CClass.UseGenericWithDType(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestGeneric_CoreLib() => AssemblyC.CClass.UseCoreLibGeneric(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestGeneric_CrossModuleDefinition() => AssemblyC.CGeneric.GetCount(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestFieldAccess_CrossModule() => AssemblyC.CClass.StaticField; - - [MethodImpl(MethodImplOptions.NoInlining)] - static int TestInterfaceDispatch_CrossModule() - { - AssemblyC.ICrossModule impl = new CrossModuleImpl(); - return impl.DoWork(); - } - - // --- Async Cross-Module Tests (runtime-async thunks) --- - - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task TestAsyncTypeRef_CrossModuleOwn() => await AssemblyC.CClass.UseOwnTypeAsync(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task TestAsyncTypeRef_Transitive() => await AssemblyC.CClass.UseDTypeAsync(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task TestAsyncMethodCall_Transitive() => await AssemblyC.CClass.CallDMethodAsync(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task TestAsyncFieldAccess_Transitive() => await AssemblyC.CClass.ReadDFieldAsync(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task TestAsyncNestedType_External() => await AssemblyC.CClass.UseNestedTypeAsync(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task TestAsyncTypeForwarder() => await AssemblyC.CClass.UseForwardedTypeAsync(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task TestAsyncGeneric_MixedOrigin() => await AssemblyC.CClass.UseGenericWithDTypeAsync(); - - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task TestAsyncGeneric_CoreLib() => await AssemblyC.CClass.UseCoreLibGenericAsync(); - - // Task-returning (void-equivalent) async variants - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task TestAsyncVoid_CrossModuleOwn() - { - await AssemblyC.CClass.UseOwnTypeAsyncVoid(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task TestAsyncVoid_Transitive() - { - await AssemblyC.CClass.UseDTypeAsyncVoid(); - } - - static void RunAllTests() - { - // Version bubble tests - Assert.AreEqual(TestTypeRef_VersionBubble(), 7); - Assert.AreEqual(TestMethodCall_VersionBubble(), 77); - Assert.AreEqual(TestFieldAccess_VersionBubble(), 777); - - // Cross-module-only tests (C's AggressiveInlining methods) - Assert.AreEqual(TestTypeRef_CrossModuleOwn(), 3); - Assert.AreEqual(TestTypeRef_Transitive(), 42); - Assert.AreEqual(TestMethodCall_Transitive(), 101); - Assert.AreEqual(TestFieldAccess_Transitive(), 100); - Assert.AreEqual(TestNestedType_External(), 99); - Assert.AreEqual(TestTypeForwarder(), "forwarded"); - Assert.AreEqual(TestGeneric_MixedOrigin(), 42); - Assert.AreEqual(TestGeneric_CoreLib(), 3); - Assert.AreEqual(TestGeneric_CrossModuleDefinition(), 1); - Assert.AreEqual(TestFieldAccess_CrossModule(), 50); - Assert.AreEqual(TestInterfaceDispatch_CrossModule(), 42); - - // Async cross-module tests (runtime-async thunks) - Assert.AreEqual(TestAsyncTypeRef_CrossModuleOwn().Result, 3); - Assert.AreEqual(TestAsyncTypeRef_Transitive().Result, 42); - Assert.AreEqual(TestAsyncMethodCall_Transitive().Result, 101); - Assert.AreEqual(TestAsyncFieldAccess_Transitive().Result, 100); - Assert.AreEqual(TestAsyncNestedType_External().Result, 99); - Assert.AreEqual(TestAsyncTypeForwarder().Result, "forwarded"); - Assert.AreEqual(TestAsyncGeneric_MixedOrigin().Result, 42); - Assert.AreEqual(TestAsyncGeneric_CoreLib().Result, 3); - - // Task-returning (void-equivalent) async tests - TestAsyncVoid_CrossModuleOwn().Wait(); - Assert.AreEqual(AssemblyC.CClass.AsyncSideEffect, 3); - TestAsyncVoid_Transitive().Wait(); - Assert.AreEqual(AssemblyC.CClass.AsyncSideEffect, 42); - } - - public static int Main() - { - // Run all tests 3x to exercise both slow and fast paths - for (int i = 0; i < 3; i++) - RunAllTests(); - - if (!Assert.HasAssertFired) - Console.WriteLine("PASSED"); - else - Console.WriteLine("FAILED"); - - return Assert.HasAssertFired ? 1 : 100; - } -} diff --git a/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj b/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj deleted file mode 100644 index 8498e89b5b199b..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/main/main_bubble.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - true - false - false - 1 - true - $(Features);runtime-async=on - $(NoWarn);CS1998 - - true - - - --opt-async-methods - main_bubble - --inputbubbleref:assemblyB.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj b/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj deleted file mode 100644 index 5c94f66283c906..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/main/main_crossmodule.csproj +++ /dev/null @@ -1,48 +0,0 @@ - - - true - false - false - 1 - true - $(Features);runtime-async=on - $(NoWarn);CS1998 - - true - - - --opt-async-methods - main_crossmodule - --opt-cross-module:assemblyC - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/tests/readytorun/crossmoduleresolution/r2rvalidate/R2RValidate.cs b/src/tests/readytorun/crossmoduleresolution/r2rvalidate/R2RValidate.cs deleted file mode 100644 index 6791f99d6639c1..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/r2rvalidate/R2RValidate.cs +++ /dev/null @@ -1,303 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; -using System.Linq; -using System.Reflection.Metadata; -using System.Reflection.Metadata.Ecma335; -using System.Reflection.PortableExecutable; -using System.Runtime.CompilerServices; -using ILCompiler.Reflection.ReadyToRun; -using Internal.ReadyToRunConstants; - -/// -/// Validates R2R images for expected cross-module inlining artifacts. -/// Usage: R2RValidate --in <r2r.dll> --ref <dir> -/// [--expect-manifest-ref <assemblyName>]... -/// [--expect-inlined <methodSubstring>]... -/// [--expect-async-thunks <methodSubstring>]... -/// [--expect-no-inlining <methodSubstring>]... -/// -class R2RValidate -{ - static int Main(string[] args) - { - string inputFile = null; - var refPaths = new List(); - var expectedManifestRefs = new List(); - var expectedInlined = new List(); - var expectedAsyncThunks = new List(); - var expectedNoInlining = new List(); - - for (int i = 0; i < args.Length; i++) - { - switch (args[i]) - { - case "--in": - inputFile = args[++i]; - break; - case "--ref": - refPaths.Add(args[++i]); - break; - case "--expect-manifest-ref": - expectedManifestRefs.Add(args[++i]); - break; - case "--expect-inlined": - expectedInlined.Add(args[++i]); - break; - case "--expect-async-thunks": - expectedAsyncThunks.Add(args[++i]); - break; - case "--expect-no-inlining": - expectedNoInlining.Add(args[++i]); - break; - default: - Console.Error.WriteLine($"Unknown argument: {args[i]}"); - return 1; - } - } - - if (inputFile is null) - { - Console.Error.WriteLine("Usage: R2RValidate --in --ref [options]"); - return 1; - } - - try - { - return Validate(inputFile, refPaths, expectedManifestRefs, expectedInlined, expectedAsyncThunks, expectedNoInlining); - } - catch (Exception ex) - { - Console.Error.WriteLine($"FAIL: {ex.Message}"); - Console.Error.WriteLine(ex.StackTrace); - return 1; - } - } - - static int Validate( - string inputFile, - List refPaths, - List expectedManifestRefs, - List expectedInlined, - List expectedAsyncThunks, - List expectedNoInlining) - { - var resolver = new SimpleAssemblyResolver(refPaths); - var reader = new ReadyToRunReader(resolver, inputFile); - - Console.WriteLine($"R2R Image: {inputFile}"); - Console.WriteLine($" Composite: {reader.Composite}"); - - // Collect all assembly references (MSIL + manifest metadata) - var allAssemblyRefs = new Dictionary(); - - if (!reader.Composite) - { - var globalReader = reader.GetGlobalMetadata().MetadataReader; - int msilRefCount = globalReader.GetTableRowCount(TableIndex.AssemblyRef); - for (int i = 1; i <= msilRefCount; i++) - { - var asmRef = globalReader.GetAssemblyReference(MetadataTokens.AssemblyReferenceHandle(i)); - string name = globalReader.GetString(asmRef.Name); - allAssemblyRefs[name] = i; - } - } - foreach (var kvp in reader.ManifestReferenceAssemblies) - allAssemblyRefs[kvp.Key] = kvp.Value; - - Console.WriteLine($" Assembly references count: {allAssemblyRefs.Count}"); - foreach (var kvp in allAssemblyRefs.OrderBy(k => k.Value)) - Console.WriteLine($" [{kvp.Value}] {kvp.Key}"); - - var methods = reader.Methods.ToList(); - Console.WriteLine($" R2R Methods count: {methods.Count}"); - - bool allPassed = true; - - // 1. Validate assembly references (MSIL + manifest) - foreach (string expected in expectedManifestRefs) - { - if (allAssemblyRefs.ContainsKey(expected)) - { - Console.WriteLine($" PASS: AssemblyRef '{expected}' found (index {allAssemblyRefs[expected]})"); - } - else - { - Console.Error.WriteLine($" FAIL: AssemblyRef '{expected}' NOT found. Available: [{string.Join(", ", allAssemblyRefs.Keys)}]"); - allPassed = false; - } - } - - // Build method lookup - // (methods already populated above for diagnostics) - - // 2. Validate CHECK_IL_BODY fixups (proof of cross-module inlining) - foreach (string pattern in expectedInlined) - { - var matching = methods.Where(m => MethodMatches(m, pattern)).ToList(); - if (matching.Count == 0) - { - Console.Error.WriteLine($" FAIL: No R2R method matching '{pattern}' found"); - allPassed = false; - continue; - } - - foreach (var method in matching) - { - bool hasCheckILBody = method.Fixups != null && - method.Fixups.Any(f => f.Signature?.FixupKind == ReadyToRunFixupKind.Check_IL_Body || - f.Signature?.FixupKind == ReadyToRunFixupKind.Verify_IL_Body); - - if (hasCheckILBody) - { - Console.WriteLine($" PASS: '{method.SignatureString}' has CHECK_IL_BODY fixup (cross-module inlining confirmed)"); - } - else - { - string fixupKinds = method.Fixups is null ? "none" : - string.Join(", ", method.Fixups.Select(f => f.Signature?.FixupKind.ToString() ?? "null")); - Console.Error.WriteLine($" FAIL: '{method.SignatureString}' has NO CHECK_IL_BODY fixup. Fixups: [{fixupKinds}]"); - allPassed = false; - } - } - } - - // 3. Validate async thunks (3+ RuntimeFunctions per method) - foreach (string pattern in expectedAsyncThunks) - { - var matching = methods.Where(m => MethodMatches(m, pattern)).ToList(); - if (matching.Count == 0) - { - Console.Error.WriteLine($" FAIL: No R2R method matching '{pattern}' found for async thunk check"); - allPassed = false; - continue; - } - - foreach (var method in matching) - { - int rtfCount = method.RuntimeFunctions.Count; - if (rtfCount >= 3) - { - Console.WriteLine($" PASS: '{method.SignatureString}' has {rtfCount} RuntimeFunctions (async thunk confirmed)"); - } - else - { - Console.Error.WriteLine($" FAIL: '{method.SignatureString}' has only {rtfCount} RuntimeFunction(s), expected >= 3 for async thunk"); - allPassed = false; - } - } - } - - // 4. Validate methods that should NOT have cross-module inlining - foreach (string pattern in expectedNoInlining) - { - var matching = methods.Where(m => MethodMatches(m, pattern)).ToList(); - if (matching.Count == 0) - { - // Method not in R2R at all — that's fine for this check - Console.WriteLine($" PASS: '{pattern}' not in R2R (no inlining, as expected)"); - continue; - } - - foreach (var method in matching) - { - bool hasCheckILBody = method.Fixups != null && - method.Fixups.Any(f => f.Signature?.FixupKind == ReadyToRunFixupKind.Check_IL_Body || - f.Signature?.FixupKind == ReadyToRunFixupKind.Verify_IL_Body); - - if (!hasCheckILBody) - { - Console.WriteLine($" PASS: '{method.SignatureString}' has no CHECK_IL_BODY fixup (no cross-module inlining, as expected)"); - } - else - { - Console.Error.WriteLine($" FAIL: '{method.SignatureString}' unexpectedly has CHECK_IL_BODY fixup"); - allPassed = false; - } - } - } - - // Summary - Console.WriteLine(); - if (allPassed) - { - Console.WriteLine($"R2R VALIDATION PASSED: {inputFile}"); - return 100; - } - else - { - Console.Error.WriteLine($"R2R VALIDATION FAILED: {inputFile}"); - return 1; - } - } - - static bool MethodMatches(ReadyToRunMethod method, string pattern) - { - string sig = method.SignatureString; - if (sig is null) - return false; - // Skip [ASYNC] and [RESUME] sub-entries — only match primary method entries - if (sig.StartsWith("[ASYNC]") || sig.StartsWith("[RESUME]")) - return false; - return sig.Contains(pattern, StringComparison.OrdinalIgnoreCase); - } -} - -/// -/// Simple assembly resolver that probes reference directories. -/// -class SimpleAssemblyResolver : IAssemblyResolver -{ - private static readonly string[] s_probeExtensions = { ".ni.exe", ".ni.dll", ".exe", ".dll" }; - private readonly List _refPaths; - - public SimpleAssemblyResolver(List refPaths) - { - _refPaths = refPaths; - } - - public IAssemblyMetadata FindAssembly(MetadataReader metadataReader, AssemblyReferenceHandle assemblyReferenceHandle, string parentFile) - { - string simpleName = metadataReader.GetString(metadataReader.GetAssemblyReference(assemblyReferenceHandle).Name); - return FindAssembly(simpleName, parentFile); - } - - public IAssemblyMetadata FindAssembly(string simpleName, string parentFile) - { - var allPaths = new List { Path.GetDirectoryName(parentFile) }; - allPaths.AddRange(_refPaths); - - foreach (string refPath in allPaths) - { - foreach (string ext in s_probeExtensions) - { - string probeFile = Path.Combine(refPath, simpleName + ext); - if (File.Exists(probeFile)) - { - try - { - return Open(probeFile); - } - catch (BadImageFormatException) - { - } - } - } - } - - return null; - } - - private static IAssemblyMetadata Open(string filename) - { - byte[] image = File.ReadAllBytes(filename); - PEReader peReader = new(Unsafe.As>(ref image)); - if (!peReader.HasMetadata) - throw new BadImageFormatException($"ECMA metadata not found in file '{filename}'"); - return new StandaloneAssemblyMetadata(peReader); - } -} diff --git a/src/tests/readytorun/crossmoduleresolution/r2rvalidate/r2rvalidate.csproj b/src/tests/readytorun/crossmoduleresolution/r2rvalidate/r2rvalidate.csproj deleted file mode 100644 index 161a29e63dd237..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/r2rvalidate/r2rvalidate.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - Exe - true - BuildOnly - 1 - - - - - - - - diff --git a/src/tests/readytorun/crossmoduleresolution/runcrossgen/RunCrossgen.cs b/src/tests/readytorun/crossmoduleresolution/runcrossgen/RunCrossgen.cs deleted file mode 100644 index 4b4a209c60c864..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/runcrossgen/RunCrossgen.cs +++ /dev/null @@ -1,210 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; - -/// -/// Orchestrates crossgen2 compilation and R2R validation for cross-module resolution tests. -/// -/// Usage: -/// RunCrossgen --crossgen2 <path> --output-dir <dir> --ref-dir <dir> -/// --assembly <name> [--extra-args <args>] (repeatable, order matters) -/// --main <name> --main-extra-args <args> --main-refs <name,...> -/// [--common-args <args>] -/// [--validate <r2rvalidate.dll path> --validate-args <args>] -/// -class RunCrossgen -{ - static int Main(string[] args) - { - string crossgen2Path = null; - string outputDir = null; - string refDir = null; - string coreRunPath = null; - string commonArgs = ""; - string mainAssembly = null; - string mainExtraArgs = ""; - string mainRefs = ""; - string validatePath = null; - string validateArgs = ""; - - var steps = new List<(string name, string extraArgs)>(); - - for (int i = 0; i < args.Length; i++) - { - switch (args[i]) - { - case "--crossgen2": - crossgen2Path = args[++i]; - break; - case "--output-dir": - outputDir = args[++i]; - break; - case "--ref-dir": - refDir = args[++i]; - break; - case "--corerun": - coreRunPath = args[++i]; - break; - case "--common-args": - commonArgs = args[++i]; - break; - case "--assembly": - string name = args[++i]; - string extra = ""; - if (i + 1 < args.Length && args[i + 1] == "--extra-args") - { - i++; - extra = args[++i]; - } - steps.Add((name, extra)); - break; - case "--main": - mainAssembly = args[++i]; - break; - case "--main-extra-args": - mainExtraArgs = args[++i]; - break; - case "--main-refs": - mainRefs = args[++i]; - break; - case "--validate": - validatePath = args[++i]; - break; - case "--validate-args": - validateArgs = args[++i]; - break; - default: - Console.Error.WriteLine($"Unknown argument: {args[i]}"); - return 1; - } - } - - if (crossgen2Path is null || outputDir is null || refDir is null || mainAssembly is null) - { - Console.Error.WriteLine("Required: --crossgen2, --output-dir, --ref-dir, --main"); - return 1; - } - - // All assembly names (deps + main) - var allAssemblies = steps.Select(s => s.name).Append(mainAssembly).ToList(); - - // 1. Copy IL assemblies to IL_DLLS/ before crossgen2 overwrites them - string ilDir = Path.Combine(outputDir, "IL_DLLS"); - Directory.CreateDirectory(ilDir); - - foreach (string asm in allAssemblies) - { - string src = Path.Combine(outputDir, $"{asm}.dll"); - string dst = Path.Combine(ilDir, $"{asm}.dll"); - if (!File.Exists(dst)) - { - if (!File.Exists(src)) - { - Console.Error.WriteLine($"FAILED: source assembly not found: {src}"); - return 1; - } - File.Copy(src, dst); - Console.WriteLine($"Copied {asm}.dll to IL_DLLS/"); - } - } - - // 2. Crossgen2 each dependency assembly - foreach (var (name, extraArgs) in steps) - { - int exitCode = RunCrossgen2(crossgen2Path, outputDir, refDir, commonArgs, - name, extraArgs); - if (exitCode != 0) - return exitCode; - } - - // 3. Crossgen2 main assembly with refs and extra args - string refArgs = string.Join(" ", mainRefs.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(r => $"-r:{r.Trim()}.dll")); - string allMainExtra = $"{refArgs} {mainExtraArgs} --map".Trim(); - - int mainExit = RunCrossgen2(crossgen2Path, outputDir, refDir, commonArgs, - mainAssembly, allMainExtra); - if (mainExit != 0) - return mainExit; - - string mapFile = Path.Combine(outputDir, $"{mainAssembly}.map"); - if (!File.Exists(mapFile)) - { - Console.Error.WriteLine($"FAILED: no map file generated at {mapFile}"); - return 1; - } - - // 4. R2R Validation (optional) - if (validatePath is not null && coreRunPath is not null && File.Exists(validatePath)) - { - Console.WriteLine($"Running R2R validation on {mainAssembly}.dll..."); - string valArgs = $"\"{validatePath}\" --in {mainAssembly}.dll --ref \"{refDir}\" --ref \"{outputDir}\" {validateArgs}"; - - int valExit = RunProcess(coreRunPath, valArgs, outputDir); - if (valExit != 100) - { - Console.Error.WriteLine($"R2R validation failed with exitcode: {valExit}"); - return 1; - } - Console.WriteLine("R2R validation passed"); - } - else if (validatePath is not null) - { - Console.WriteLine($"WARNING: r2rvalidate not found at {validatePath}, skipping"); - } - - Console.WriteLine("Crossgen orchestration completed successfully"); - return 0; - } - - static int RunCrossgen2(string crossgen2Path, string outputDir, string refDir, - string commonArgs, string assemblyName, string extraArgs) - { - string ilDll = Path.Combine("IL_DLLS", $"{assemblyName}.dll"); - string outDll = $"{assemblyName}.dll"; - - // Use glob pattern for reference directory - string refGlob = Path.Combine(refDir, "*.dll"); - string arguments = $"{commonArgs} -r:\"{refGlob}\" {extraArgs} -o:{outDll} {ilDll}".Trim(); - - Console.WriteLine($"Crossgen2 {assemblyName}..."); - int exitCode = RunProcess(crossgen2Path, arguments, outputDir); - - if (exitCode != 0) - { - Console.Error.WriteLine($"Crossgen2 {assemblyName} failed with exitcode: {exitCode}"); - return 1; - } - - return 0; - } - - static int RunProcess(string fileName, string arguments, string workingDir) - { - var psi = new ProcessStartInfo - { - FileName = fileName, - Arguments = arguments, - WorkingDirectory = workingDir, - UseShellExecute = false, - }; - - // Suppress DOTNET variables that interfere with crossgen2 - psi.Environment.Remove("DOTNET_GCName"); - psi.Environment.Remove("DOTNET_GCStress"); - psi.Environment.Remove("DOTNET_HeapVerify"); - psi.Environment.Remove("DOTNET_ReadyToRun"); - - Console.WriteLine($" > {fileName} {arguments}"); - - using var process = Process.Start(psi); - process.WaitForExit(); - - return process.ExitCode; - } -} diff --git a/src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj b/src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj deleted file mode 100644 index 34ae58fa6638f1..00000000000000 --- a/src/tests/readytorun/crossmoduleresolution/runcrossgen/runcrossgen.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - Exe - BuildOnly - 1 - - $(CORE_ROOT)/runcrossgen - false - false - - $(NETCoreSdkRuntimeIdentifier) - - - - - From 537cd1b323f751b7b9b4457e6203ed431b3f6b8b Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:40:51 -0700 Subject: [PATCH 27/74] Fix stale and inaccurate comments across R2R test files - R2RResultChecker: fix broken see-cref (R2RTestCase.Validate -> CrossgenCompilation.Validate) - R2RTestRunner: fix comment on BuildReferencePaths (runtime pack only, not compiled assemblies) - CrossModuleInliningInfoSection: fix version to v6.2 per readytorun.h - AsyncMethods.cs: describe actual test behavior (cross-module inlining, not RuntimeFunction layout) - BasicAsyncEmission.cs: remove false claim about resumption stubs - AsyncDevirtualize.cs: describe actual validation ([ASYNC] variants, not devirtualized calls) - AsyncCrossModule.cs: describe actual validation (manifest refs + [ASYNC] variants) - AsyncCrossModuleContinuation.cs: remove false claim about ContinuationLayout fixups - R2RTestSuites: fix 'and' -> 'or' for inlining info sections, fix A+B -> A ref, clarify devirt test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/CrossModuleInlining/AsyncMethods.cs | 6 +++--- .../TestCases/R2RTestSuites.cs | 10 +++++----- .../TestCases/RuntimeAsync/AsyncCrossModule.cs | 4 ++-- .../RuntimeAsync/AsyncCrossModuleContinuation.cs | 4 ++-- .../TestCases/RuntimeAsync/AsyncDevirtualize.cs | 4 ++-- .../TestCases/RuntimeAsync/BasicAsyncEmission.cs | 4 ++-- .../TestCasesRunner/R2RResultChecker.cs | 2 +- .../TestCasesRunner/R2RTestRunner.cs | 2 +- .../CrossModuleInliningInfoSection.cs | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs index b9efb76fab2646..fdf1b159130b95 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs @@ -1,6 +1,6 @@ -// Test: Async method thunks in R2R -// Validates that runtime-async compiled methods produce the expected -// RuntimeFunction layout (thunk + async body + resumption stub). +// Test: Cross-module inlining of async methods +// Validates that async methods from AsyncInlineableLib are cross-module +// inlined into this assembly with CHECK_IL_BODY fixups. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index f21d18c2db9bab..b7686c1ca37f9f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -243,8 +243,8 @@ static void Validate(ReadyToRunReader reader) } /// - /// PR #125420: Devirtualization of async methods through - /// AsyncAwareVirtualMethodResolutionAlgorithm. + /// PR #125420: [ASYNC] variant generation for devirtualizable async call patterns + /// (sealed class and interface dispatch through AsyncAwareVirtualMethodResolutionAlgorithm). /// [Fact] public void RuntimeAsyncDevirtualize() @@ -368,8 +368,8 @@ static void Validate(ReadyToRunReader reader) /// /// Composite mode with sync cross-module inlining. - /// Validates that InliningInfo2 and CrossModuleInlineInfo sections - /// are properly populated (CompositeBasic only validates ManifestRef). + /// Validates that inlining info (CrossModuleInlineInfo or InliningInfo2) is + /// properly populated (CompositeBasic only validates ManifestRef). /// [Fact] public void CompositeCrossModuleInlining() @@ -552,7 +552,7 @@ static void Validate(ReadyToRunReader reader) } /// - /// Two-step compilation: composite A+B, then non-composite C referencing A+B. + /// Two-step compilation: composite A+B, then non-composite C referencing A. /// Exercises the multi-compilation model. /// [Fact] diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs index 28534b5a9fcf16..cc1853b48a5bb7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs @@ -1,6 +1,6 @@ // Test: Cross-module async method inlining -// Validates that async methods from a dependency library can be -// cross-module inlined, creating manifest refs and CHECK_IL_BODY fixups. +// Validates that cross-module async compilation produces manifest refs +// and [ASYNC] variants for methods calling into a dependency library. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs index d22d7c76c881b0..b4e2f87ecda45d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs @@ -1,6 +1,6 @@ // Test: Non-composite runtime-async cross-module inlining with continuation layouts. -// The inlinee methods capture GC refs across await points, which forces -// ContinuationLayout fixups that reference cross-module types. +// The dependency methods capture GC refs across await points. +// Validates manifest refs and [ASYNC] variants for cross-module async calls. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs index 76d69eecf2fc3f..6170e4ac67360a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs @@ -1,6 +1,6 @@ // Test: Async virtual method devirtualization in R2R -// Validates that sealed class and interface dispatch of async methods -// produces devirtualized direct call entries in the R2R image. +// Validates that async methods on sealed/interface types +// produce [ASYNC] variant entries in the R2R image. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs index c6137cfc309188..94a314a1b101fb 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs @@ -1,6 +1,6 @@ // Test: Basic async method emission in R2R -// Validates that runtime-async methods produce [ASYNC] variant entries and -// resumption stubs in the R2R image. +// Validates that runtime-async methods produce [ASYNC] variant entries +// in the R2R image. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index 975ae479201b52..7f35f520b7da74 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -16,7 +16,7 @@ namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; /// /// Static assertion helpers for validating R2R images via . -/// Use these in callbacks. +/// Use these in callbacks. /// internal static class R2RAssert { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs index 867a5158da73a5..aa0dec65bf9cad 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs @@ -295,7 +295,7 @@ private static string RunCrossgenCompilation( foreach (var option in compilation.Options) args.Add(option.ToArg()); - // Global refs (all compiled IL assemblies + runtime pack) + // Global refs (runtime pack + System.Private.CoreLib) AddRefArgs(args, refPaths); EnsureDirectoryExists(Path.GetDirectoryName(outputFile)); diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs index b19adfc4e4be56..2a9cde6b501ec4 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs @@ -13,7 +13,7 @@ namespace ILCompiler.Reflection.ReadyToRun { /// - /// Parser for the CrossModuleInlineInfo section (ReadyToRunSectionType 119, added in R2R v6.3). + /// Parser for the CrossModuleInlineInfo section (ReadyToRunSectionType 119, added in R2R v6.2). /// This format differs from InliningInfo2 (section 114) — it uses a stream-size counted /// encoding with 2-bit flags on the inlinee index and supports ILBody import indices for /// cross-module inlinees and inliners. From 54a3bb628124ed16c6d54ff7a08cc9d2455f3ada Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:07:30 -0700 Subject: [PATCH 28/74] Enable async method variants in composite mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the two gates that prevented async method variants (runtime-async) from being compiled in composite R2R mode: 1. ReadyToRunILProvider.NeedsCrossModuleInlineableTokens: Removed the '!VersionsWithModule(SystemModule)' condition for async stubs. Async thunks and resumption stubs use compiler-generated IL with synthetic tokens created by ILEmitter.NewToken() that ALWAYS need rewriting into the MutableModule via ManifestModuleWrappedMethodIL, regardless of whether CoreLib is in the version bubble. 2. CorInfoImpl.ShouldSkipCompilation: Removed the blanket composite-mode skip for IsCompilerGeneratedILBodyForAsync() methods. This was a workaround for the missing wrapping in (1) — with the root cause fixed, async stubs compile correctly in composite mode. Fix two test issues: - CompositeAsyncCrossModuleInlining: Validate sync inlining (GetValueSync) instead of async inlining (await doesn't produce traditional inlining). - MultiStepCompositeAndNonCompositeAsync: Fix assembly reference mismatch by adding a proper MultiStepAsyncConsumer source that calls AsyncCompositeLib. All 20 R2R tests pass including the 5 previously-skipped composite async tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MultiStepAsyncConsumer.cs | 22 ++++++++++++++++ .../TestCases/R2RTestSuites.cs | 26 +++++++++---------- .../IL/ReadyToRunILProvider.cs | 7 +++-- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 9 ------- 4 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs new file mode 100644 index 00000000000000..14d541a360d6a3 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs @@ -0,0 +1,22 @@ +// Test: Non-composite consumer of composite async methods. +// Calls AsyncCompositeLib methods and has its own async methods, +// exercising multi-step compilation (composite then non-composite) +// with runtime-async in both steps. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class MultiStepAsyncConsumer +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task ConsumeCompositeAsync() + { + return await AsyncCompositeLib.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task ConsumeCompositeStringAsync() + { + return await AsyncCompositeLib.GetStringAsync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index b7686c1ca37f9f..618369cc942e23 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -411,7 +411,7 @@ static void Validate(ReadyToRunReader reader) /// Composite mode with runtime-async methods in both assemblies. /// Validates async variants exist in composite output. /// - [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + [Fact] public void CompositeAsync() { var asyncCompositeLib = new CompiledAssembly @@ -451,12 +451,12 @@ static void Validate(ReadyToRunReader reader) } /// - /// The full intersection: composite + runtime-async + cross-module inlining. - /// Async methods from AsyncCompositeLib are inlined into CompositeAsyncMain - /// within a composite image, exercising MutableModule token encoding for - /// cross-module async continuation layouts. + /// Composite + runtime-async + cross-module inlining. + /// Both assemblies are part of the composite image, so sync methods inline + /// across modules. Async methods produce [ASYNC] variants and continuation + /// layouts but are not traditionally inlined (await introduces async plumbing). /// - [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + [Fact] public void CompositeAsyncCrossModuleInlining() { var asyncCompositeLib = new CompiledAssembly @@ -491,7 +491,7 @@ static void Validate(ReadyToRunReader reader) { R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); - R2RAssert.HasInlinedMethod(reader, "CallCompositeAsync", "GetValueAsync"); + R2RAssert.HasInlinedMethod(reader, "CallCompositeSync", "GetValueSync"); R2RAssert.HasContinuationLayout(reader, "CallCompositeAsync"); } } @@ -618,7 +618,7 @@ public void MultiStepCompositeAndNonComposite() /// Composite + runtime-async + cross-module devirtualization. /// Interface defined in AsyncInterfaceLib, call sites in CompositeAsyncDevirtMain. /// - [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + [Fact] public void CompositeAsyncDevirtualize() { var asyncInterfaceLib = new CompiledAssembly @@ -771,7 +771,7 @@ static void Validate(ReadyToRunReader reader) /// Composite + runtime-async + transitive (3 assemblies). /// Full combination of composite, async, and transitive references. /// - [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + [Fact] public void CompositeAsyncTransitive() { var asyncExternalLib = new CompiledAssembly @@ -827,9 +827,9 @@ static void Validate(ReadyToRunReader reader) /// /// Multi-step compilation with runtime-async in all assemblies. /// Step 1: Composite of async libs. Step 2: Non-composite consumer - /// with cross-module inlining of async methods. + /// that calls async methods from the composite lib. /// - [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + [Fact] public void MultiStepCompositeAndNonCompositeAsync() { var asyncCompositeLib = new CompiledAssembly @@ -850,7 +850,7 @@ public void MultiStepCompositeAndNonCompositeAsync() AssemblyName = "MultiStepAsyncConsumer", SourceResourceNames = [ - "RuntimeAsync/AsyncCrossModuleContinuation.cs", + "CrossModuleInlining/MultiStepAsyncConsumer.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -886,7 +886,7 @@ public void MultiStepCompositeAndNonCompositeAsync() Validate = reader => { R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); - R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef"); + R2RAssert.HasAsyncVariant(reader, "ConsumeCompositeAsync"); }, }, ])); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs index 5460cad265c0d6..1bd654144576fe 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs @@ -196,8 +196,11 @@ public bool NeedsCrossModuleInlineableTokens(MethodDesc method) { bool regularCrossModuleInlineable = (!_compilationModuleGroup.VersionsWithMethodBody(method) && _compilationModuleGroup.CrossModuleInlineable(method)); - bool requiredCrossModuleInliningForAsync = (NeedsTaskReturningThunk(method) || NeedsAsyncThunk(method) || method is AsyncResumptionStub) - && !_compilationModuleGroup.VersionsWithModule(method.Context.SystemModule); + // Async thunks and resumption stubs always need wrapping because they use + // compiler-generated IL with synthetic tokens that must be rewritten into the + // MutableModule. This is required regardless of whether CoreLib is in the + // version bubble (e.g. composite mode). + bool requiredCrossModuleInliningForAsync = NeedsTaskReturningThunk(method) || NeedsAsyncThunk(method) || method is AsyncResumptionStub; if ((regularCrossModuleInlineable || requiredCrossModuleInliningForAsync) && !_manifestModuleWrappedMethods.ContainsKey(method)) { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index cb670ba2db0cbe..95cfdb523a4277 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -564,15 +564,6 @@ public static bool ShouldSkipCompilation(InstructionSetSupport instructionSetSup // Special methods on delegate types return true; } - // Async resumption stubs use faux IL with synthetic tokens. When CoreLib is in the - // version bubble the stubs are not wrapped with ManifestModuleWrappedMethodIL, so - // token resolution for InstantiatedType / ParameterizedType falls through to a path - // that cannot handle them. Skip compilation and let the runtime JIT these stubs. - // https://github.com/dotnet/runtime/issues/125337 - if (methodNeedingCode.IsCompilerGeneratedILBodyForAsync() && compilation != null && compilation.NodeFactory.CompilationModuleGroup.IsCompositeBuildMode) - { - return true; - } if (ShouldCodeNotBeCompiledIntoFinalImage(instructionSetSupport, methodNeedingCode)) { return true; From 0aa5103b4d4c1492f05cbdce81a8480e9fd1d7cc Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:12:12 -0700 Subject: [PATCH 29/74] Remove unused using System directive in MultiStepAsyncConsumer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs index 14d541a360d6a3..20f972ecf9db2b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs @@ -2,7 +2,6 @@ // Calls AsyncCompositeLib methods and has its own async methods, // exercising multi-step compilation (composite then non-composite) // with runtime-async in both steps. -using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; From 40a61b26d885e7c78d9c82dbc166c22ba38c99f6 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:28:30 -0700 Subject: [PATCH 30/74] Revert async method variant enablement in composite mode Runtime-async thunks in composite R2R still produce execution failures (FileNotFoundException from async state machine code). Revert the two commits that removed the composite-mode gates until the underlying runtime-async feature is ready. Reverts: 54a3bb62812, 0aa5103b4d4 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MultiStepAsyncConsumer.cs | 21 --------------- .../TestCases/R2RTestSuites.cs | 26 +++++++++---------- .../IL/ReadyToRunILProvider.cs | 7 ++--- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 9 +++++++ 4 files changed, 24 insertions(+), 39 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs deleted file mode 100644 index 20f972ecf9db2b..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepAsyncConsumer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Test: Non-composite consumer of composite async methods. -// Calls AsyncCompositeLib methods and has its own async methods, -// exercising multi-step compilation (composite then non-composite) -// with runtime-async in both steps. -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class MultiStepAsyncConsumer -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task ConsumeCompositeAsync() - { - return await AsyncCompositeLib.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task ConsumeCompositeStringAsync() - { - return await AsyncCompositeLib.GetStringAsync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 618369cc942e23..b7686c1ca37f9f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -411,7 +411,7 @@ static void Validate(ReadyToRunReader reader) /// Composite mode with runtime-async methods in both assemblies. /// Validates async variants exist in composite output. /// - [Fact] + [Fact(Skip = "Runtime-async methods are not generated in composite mode")] public void CompositeAsync() { var asyncCompositeLib = new CompiledAssembly @@ -451,12 +451,12 @@ static void Validate(ReadyToRunReader reader) } /// - /// Composite + runtime-async + cross-module inlining. - /// Both assemblies are part of the composite image, so sync methods inline - /// across modules. Async methods produce [ASYNC] variants and continuation - /// layouts but are not traditionally inlined (await introduces async plumbing). + /// The full intersection: composite + runtime-async + cross-module inlining. + /// Async methods from AsyncCompositeLib are inlined into CompositeAsyncMain + /// within a composite image, exercising MutableModule token encoding for + /// cross-module async continuation layouts. /// - [Fact] + [Fact(Skip = "Runtime-async methods are not generated in composite mode")] public void CompositeAsyncCrossModuleInlining() { var asyncCompositeLib = new CompiledAssembly @@ -491,7 +491,7 @@ static void Validate(ReadyToRunReader reader) { R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); - R2RAssert.HasInlinedMethod(reader, "CallCompositeSync", "GetValueSync"); + R2RAssert.HasInlinedMethod(reader, "CallCompositeAsync", "GetValueAsync"); R2RAssert.HasContinuationLayout(reader, "CallCompositeAsync"); } } @@ -618,7 +618,7 @@ public void MultiStepCompositeAndNonComposite() /// Composite + runtime-async + cross-module devirtualization. /// Interface defined in AsyncInterfaceLib, call sites in CompositeAsyncDevirtMain. /// - [Fact] + [Fact(Skip = "Runtime-async methods are not generated in composite mode")] public void CompositeAsyncDevirtualize() { var asyncInterfaceLib = new CompiledAssembly @@ -771,7 +771,7 @@ static void Validate(ReadyToRunReader reader) /// Composite + runtime-async + transitive (3 assemblies). /// Full combination of composite, async, and transitive references. /// - [Fact] + [Fact(Skip = "Runtime-async methods are not generated in composite mode")] public void CompositeAsyncTransitive() { var asyncExternalLib = new CompiledAssembly @@ -827,9 +827,9 @@ static void Validate(ReadyToRunReader reader) /// /// Multi-step compilation with runtime-async in all assemblies. /// Step 1: Composite of async libs. Step 2: Non-composite consumer - /// that calls async methods from the composite lib. + /// with cross-module inlining of async methods. /// - [Fact] + [Fact(Skip = "Runtime-async methods are not generated in composite mode")] public void MultiStepCompositeAndNonCompositeAsync() { var asyncCompositeLib = new CompiledAssembly @@ -850,7 +850,7 @@ public void MultiStepCompositeAndNonCompositeAsync() AssemblyName = "MultiStepAsyncConsumer", SourceResourceNames = [ - "CrossModuleInlining/MultiStepAsyncConsumer.cs", + "RuntimeAsync/AsyncCrossModuleContinuation.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -886,7 +886,7 @@ public void MultiStepCompositeAndNonCompositeAsync() Validate = reader => { R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); - R2RAssert.HasAsyncVariant(reader, "ConsumeCompositeAsync"); + R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef"); }, }, ])); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs index 1bd654144576fe..5460cad265c0d6 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs @@ -196,11 +196,8 @@ public bool NeedsCrossModuleInlineableTokens(MethodDesc method) { bool regularCrossModuleInlineable = (!_compilationModuleGroup.VersionsWithMethodBody(method) && _compilationModuleGroup.CrossModuleInlineable(method)); - // Async thunks and resumption stubs always need wrapping because they use - // compiler-generated IL with synthetic tokens that must be rewritten into the - // MutableModule. This is required regardless of whether CoreLib is in the - // version bubble (e.g. composite mode). - bool requiredCrossModuleInliningForAsync = NeedsTaskReturningThunk(method) || NeedsAsyncThunk(method) || method is AsyncResumptionStub; + bool requiredCrossModuleInliningForAsync = (NeedsTaskReturningThunk(method) || NeedsAsyncThunk(method) || method is AsyncResumptionStub) + && !_compilationModuleGroup.VersionsWithModule(method.Context.SystemModule); if ((regularCrossModuleInlineable || requiredCrossModuleInliningForAsync) && !_manifestModuleWrappedMethods.ContainsKey(method)) { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 95cfdb523a4277..cb670ba2db0cbe 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -564,6 +564,15 @@ public static bool ShouldSkipCompilation(InstructionSetSupport instructionSetSup // Special methods on delegate types return true; } + // Async resumption stubs use faux IL with synthetic tokens. When CoreLib is in the + // version bubble the stubs are not wrapped with ManifestModuleWrappedMethodIL, so + // token resolution for InstantiatedType / ParameterizedType falls through to a path + // that cannot handle them. Skip compilation and let the runtime JIT these stubs. + // https://github.com/dotnet/runtime/issues/125337 + if (methodNeedingCode.IsCompilerGeneratedILBodyForAsync() && compilation != null && compilation.NodeFactory.CompilationModuleGroup.IsCompositeBuildMode) + { + return true; + } if (ShouldCodeNotBeCompiledIntoFinalImage(instructionSetSupport, methodNeedingCode)) { return true; From ce4ca00746ac4e7ac47dd29e25648ab502900b5a Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:01:30 -0700 Subject: [PATCH 31/74] WIP: Execution infrastructure, ActiveIssue, and MethodDefEntryPointsTable - Add corerun execution support via testhost shared framework - Switch Fact(Skip) to ActiveIssue for composite async tests - Add MethodDefEntryPointsTable projection to reflection library - Simplify R2RDriver.Compile, add OutputKind to CompiledAssembly - Add TestHostSharedFrameworkDir and CorerunPath to TestPaths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ILCompiler.ReadyToRun.Tests.csproj | 8 +- .../CrossModuleInlining/CompositeAsync.cs | 9 ++ .../TestCases/R2RTestSuites.cs | 27 ++-- .../TestCasesRunner/R2RDriver.cs | 8 +- .../TestCasesRunner/R2RResultChecker.cs | 18 +++ .../TestCasesRunner/R2RTestCaseCompiler.cs | 4 +- .../TestCasesRunner/R2RTestRunner.cs | 70 ++++++++ .../TestCasesRunner/TestPaths.cs | 37 +++++ .../MethodDefEntryPointsTable.cs | 150 ++++++++++++++++++ .../{NativeArray.cs => NativeSparseArray.cs} | 4 +- .../ReadyToRunReader.cs | 6 +- 11 files changed, 317 insertions(+), 24 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs rename src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/{NativeArray.cs => NativeSparseArray.cs} (96%) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj index 24c1d7f9a7413f..9398f04c97b171 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj @@ -23,9 +23,12 @@ - + + + @@ -42,6 +45,9 @@ $(CoreCLRArtifactsPath) + + $(NetCoreAppCurrentTestHostSharedFrameworkPath) + $(TargetArchitecture) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs index 7e19cc7eea200c..f315a93d6269b9 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs @@ -6,6 +6,15 @@ public static class CompositeAsyncMain { + public static int Main() + { + int sync = CallCompositeSync(); + if (sync != 99) + return 1; + + return 0; + } + [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallCompositeAsync() { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index b7686c1ca37f9f..7388cb5d23fc7b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -1,11 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable xUnit1004 // Test methods should not be skipped — composite+async tests are intentionally deferred - using System.Collections.Generic; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; using ILCompiler.Reflection.ReadyToRun; +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; @@ -411,7 +411,8 @@ static void Validate(ReadyToRunReader reader) /// Composite mode with runtime-async methods in both assemblies. /// Validates async variants exist in composite output. /// - [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] + [Fact] public void CompositeAsync() { var asyncCompositeLib = new CompiledAssembly @@ -425,20 +426,24 @@ public void CompositeAsync() AssemblyName = "CompositeAsyncMain", SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], Features = { RuntimeAsyncFeature }, - References = [asyncCompositeLib] + References = [asyncCompositeLib], + OutputKind = OutputKind.ConsoleApplication }; + var compositeAsyncMainCG2 = new CrossgenAssembly(compositeAsyncMain); + new R2RTestRunner(_output).Run(new R2RTestCase( nameof(CompositeAsync), [ new(nameof(CompositeAsync), [ new CrossgenAssembly(asyncCompositeLib), - new CrossgenAssembly(compositeAsyncMain), + compositeAsyncMainCG2 ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = Validate, + Execute = compositeAsyncMainCG2, }, ])); @@ -456,7 +461,8 @@ static void Validate(ReadyToRunReader reader) /// within a composite image, exercising MutableModule token encoding for /// cross-module async continuation layouts. /// - [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] + [Fact] public void CompositeAsyncCrossModuleInlining() { var asyncCompositeLib = new CompiledAssembly @@ -618,7 +624,8 @@ public void MultiStepCompositeAndNonComposite() /// Composite + runtime-async + cross-module devirtualization. /// Interface defined in AsyncInterfaceLib, call sites in CompositeAsyncDevirtMain. /// - [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] + [Fact] public void CompositeAsyncDevirtualize() { var asyncInterfaceLib = new CompiledAssembly @@ -771,7 +778,8 @@ static void Validate(ReadyToRunReader reader) /// Composite + runtime-async + transitive (3 assemblies). /// Full combination of composite, async, and transitive references. /// - [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] + [Fact] public void CompositeAsyncTransitive() { var asyncExternalLib = new CompiledAssembly @@ -829,7 +837,8 @@ static void Validate(ReadyToRunReader reader) /// Step 1: Composite of async libs. Step 2: Non-composite consumer /// with cross-module inlining of async methods. /// - [Fact(Skip = "Runtime-async methods are not generated in composite mode")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] + [Fact] public void MultiStepCompositeAndNonCompositeAsync() { var asyncCompositeLib = new CompiledAssembly diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs index b61f978b3112e8..3f2765f05b834d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs @@ -98,13 +98,7 @@ public R2RDriver(ITestOutputHelper output) /// /// Runs crossgen2 with the given arguments. /// - public R2RCompilationResult Compile(List args) - { - var fullArgs = new List(args); - return RunCrossgen2(fullArgs); - } - - private R2RCompilationResult RunCrossgen2(List crossgen2Args) + public R2RCompilationResult Compile(List crossgen2Args) { var psi = new ProcessStartInfo(TestPaths.Crossgen2Exe, crossgen2Args) { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index 7f35f520b7da74..9e7d0ec85930c6 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -89,6 +89,24 @@ public static void HasCrossModuleInlinedMethod(ReadyToRunReader reader, string i $"Recorded inlining pairs:\n {string.Join("\n ", allPairs)}"); } + /// + /// Asserts that the specified method has an entry in the import sections with a fixup for this method that has a ModuleOverride to the mutable module, + /// and method signature that resolves to the specified resolvedEntity + /// + public static void MethodHasMutableModuleFixupToken(ReadyToRunReader reader, string methodName, string resolvedEntity) + { + foreach(var section in reader.ImportSections) + { + foreach(var entry in section.Entries) + { + } + } + + Assert.Fail($"Method '{methodName}' with mutable module fixup token '{resolvedEntity}' not found."); + + } + + /// /// Asserts that the CrossModuleInlineInfo section has an entry for an inlinee matching /// with cross-module inliners whose resolved names diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs index 8293a31a82e513..b4801fc8f22583 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs @@ -57,8 +57,8 @@ public string CompileAssembly( string assemblyName, IEnumerable sources, string outputPath, - IEnumerable? additionalReferences = null, - OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, + OutputKind outputKind, + IEnumerable additionalReferences, IEnumerable? additionalDefines = null, IEnumerable>? features = null) { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs index aa0dec65bf9cad..4b829aef41d48c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using ILCompiler.Reflection.ReadyToRun; +using Microsoft.CodeAnalysis; using Xunit; using Xunit.Abstractions; @@ -34,6 +36,8 @@ internal sealed class CompiledAssembly /// public List References { get; init; } = new(); + public OutputKind OutputKind {get; init;} = OutputKind.DynamicallyLinkedLibrary; + private string? _outputDir = null; public string FilePath => _outputDir != null ? Path.Combine(_outputDir, "IL", AssemblyName + ".dll") : throw new InvalidOperationException("Output directory not set"); @@ -89,6 +93,14 @@ internal sealed class CrossgenCompilation(string name, List as /// public Action? Validate { get; init; } + /// + /// When set, the R2R image is executed with corerun after validation. + /// The value is the name of the entry-point assembly (must have been + /// compiled as ). + /// Exit code 0 = pass; non-zero fails the test. + /// + public CrossgenAssembly? Execute { get; init; } = null; + public string Name => name; public bool IsComposite => Options.Contains(Crossgen2Option.Composite); @@ -127,6 +139,9 @@ internal sealed class R2RTestCase(string name, List compila /// public List Compilations => compilations; + /// + /// Returns a list of assemblies to compile with Roslyn, in such an order that dependencies are compiled before the assemblies that depend on them. + /// public IEnumerable GetAssemblies() { // Should be a small number of assemblies, so a simple list is fine as an insertion-ordered set @@ -207,6 +222,60 @@ public void Run(R2RTestCase testCase) var reader = new ReadyToRunReader(new SimpleAssemblyResolver(), outputPath); compilation.Validate(reader); } + if (compilation.Execute is not null) + { + string exeAssemblyName = compilation.Execute.ILAssembly.AssemblyName; + string cg2Dir = Path.GetDirectoryName(outputPath)!; + + // Copy dependency IL assemblies next to the R2R exe so corerun can probe them. + foreach (var asm in compilation.Assemblies) + { + if (asm == compilation.Execute) + continue; + string src = asm.ILAssembly.FilePath; + string dest = Path.Combine(cg2Dir, Path.GetFileName(src)); + if (File.Exists(src) && !File.Exists(dest)) + File.Copy(src, dest); + } + + string exePath = Path.Combine(cg2Dir, exeAssemblyName + ".dll"); + Assert.True(File.Exists(exePath), $"Entry-point assembly not found: {exePath}"); + + // Use corerun from the testhost shared framework dir with CORE_ROOT + // pointing there. It has corerun, libcoreclr, managed framework DLLs, + // and native shims (libSystem.Native, etc.) — all in one directory. + _output.WriteLine($" Executing R2R image with corerun: {exePath}"); + var psi = new ProcessStartInfo(TestPaths.CorerunPath, [exePath]) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(psi)!; + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + + if (!process.WaitForExit(TimeSpan.FromSeconds(30))) + { + try { process.Kill(entireProcessTree: true); } + catch { /* best effort */ } + Assert.Fail($"Execution of '{exeAssemblyName}' timed out after 30 seconds"); + } + + string stdout = stdoutTask.GetAwaiter().GetResult(); + string stderr = stderrTask.GetAwaiter().GetResult(); + + if (!string.IsNullOrWhiteSpace(stdout)) + _output.WriteLine($" stdout: {stdout}"); + if (!string.IsNullOrWhiteSpace(stderr)) + _output.WriteLine($" stderr: {stderr}"); + + Assert.True(process.ExitCode == 0, + $"Execution of '{exeAssemblyName}' failed (exit code {process.ExitCode}):\n{stderr}"); + } + } } finally @@ -237,6 +306,7 @@ private Dictionary CompileAllAssemblies( asm.AssemblyName, sources, asm.FilePath, + asm.OutputKind, additionalReferences: asm.References.Select(r => r.FilePath).ToList(), features: asm.Features.Count > 0 ? asm.Features : null); paths[asm.AssemblyName] = ilPath; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs index ecdd840e6b0143..bd5f845111d280 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs @@ -142,6 +142,43 @@ public static string RefPackDir /// public static string TargetTriple => $"{TargetOS.ToLowerInvariant()}-{TargetArchitecture.ToLowerInvariant()}"; + /// + /// Path to the testhost shared framework directory containing corerun, + /// libcoreclr, all managed framework DLLs, and native shims. + /// e.g. artifacts/bin/testhost/net11.0-linux-Release-x64/shared/Microsoft.NETCore.App/11.0.0/ + /// + public static string TestHostSharedFrameworkDir + { + get + { + string dir = GetRequiredConfig("R2RTest.TestHostSharedFrameworkDir"); + if (!Directory.Exists(dir)) + { + foreach (string fallbackConfig in new[] { "Release", "Debug", "Checked" }) + { + string fallback = Regex.Replace( + dir, @"-(Debug|Release|Checked)-", $"-{fallbackConfig}-"); + if (Directory.Exists(fallback)) + return fallback; + } + } + + return dir; + } + } + + /// + /// Path to the corerun executable inside the testhost shared framework. + /// + public static string CorerunPath + { + get + { + string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "corerun.exe" : "corerun"; + return Path.Combine(TestHostSharedFrameworkDir, exe); + } + } + /// /// Returns all framework reference assembly paths (*.dll in the runtime pack). /// diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs new file mode 100644 index 00000000000000..5d924111f08934 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace ILCompiler.Reflection.ReadyToRun +{ + /// + /// Structural projection of the MethodDefEntryPoints NativeArray section. + /// Each entry maps a MethodDef RID to its RuntimeFunction index and the + /// fixup cell references (import section table + cell index pairs) it needs + /// resolved before execution. + /// + public sealed class MethodDefEntryPointsTable + { + public IReadOnlyList Entries { get; } + + private MethodDefEntryPointsTable(List entries) + { + Entries = entries; + } + + /// + /// Parse a MethodDefEntryPoints section from the R2R image. + /// + /// The R2R reader containing the image. + /// The MethodDefEntryPoints section to parse. + public static MethodDefEntryPointsTable Parse(ReadyToRunReader reader, ReadyToRunSection section) + { + int sectionOffset = reader.GetOffset(section.RelativeVirtualAddress); + NativeSparseArray methodEntryPoints = new NativeSparseArray(reader.ImageReader, (uint)sectionOffset); + uint count = methodEntryPoints.GetCount(); + + var entries = new List((int)count); + + for (uint rid = 1; rid <= count; rid++) + { + int offset = 0; + if (!methodEntryPoints.TryGetAt(rid - 1, ref offset)) + { + continue; + } + + // Decode the entry point: compressed uint with RuntimeFunction index + fixup flag + uint id = 0; + offset = (int)reader.ImageReader.DecodeUnsigned((uint)offset, ref id); + + int? fixupOffset = null; + uint runtimeFunctionIndex; + + if ((id & 1) != 0) + { + // Has fixups + if ((id & 2) != 0) + { + // Fixup list uses a backward offset + uint val = 0; + reader.ImageReader.DecodeUnsigned((uint)offset, ref val); + offset -= (int)val; + } + + fixupOffset = offset; + runtimeFunctionIndex = id >> 2; + } + else + { + runtimeFunctionIndex = id >> 1; + } + + // Parse the fixup delay list (nibble-encoded import section/slot pairs) + var fixupCells = new List(); + if (fixupOffset.HasValue) + { + NibbleReader nibbleReader = new NibbleReader(reader.ImageReader, fixupOffset.Value); + uint curTableIndex = nibbleReader.ReadUInt(); + + while (true) + { + uint cellIndex = nibbleReader.ReadUInt(); + + while (true) + { + fixupCells.Add(new FixupCellRef(curTableIndex, cellIndex)); + + uint delta = nibbleReader.ReadUInt(); + if (delta == 0) + break; + + cellIndex += delta; + } + + uint tableDelta = nibbleReader.ReadUInt(); + if (tableDelta == 0) + break; + + curTableIndex += tableDelta; + } + } + + entries.Add(new MethodDefEntry(rid, runtimeFunctionIndex, fixupCells)); + } + + return new MethodDefEntryPointsTable(entries); + } + } + + /// + /// One entry in the MethodDefEntryPoints table: a compiled method identified by + /// its MethodDef RID, with its RuntimeFunction index and fixup cell references. + /// Null entries in indicate + /// MethodDef RIDs that have no compiled entrypoint. + /// + public sealed class MethodDefEntry + { + /// MethodDef RID (1-based). + public uint Rid { get; } + + /// Index into the RuntimeFunctions array. + public uint EntryPointIndex { get; } + + /// Fixup cells this method needs resolved before execution. + public IReadOnlyList FixupCells { get; } + + public MethodDefEntry(uint rid, uint entryPointIndex, List fixupCells) + { + Rid = rid; + EntryPointIndex = entryPointIndex; + FixupCells = fixupCells; + } + } + + /// + /// A reference to a single fixup cell: identifies the import section and + /// entry index within that section. + /// + public sealed class FixupCellRef + { + /// Index of the import section in the ImportSections array. + public uint TableIndex { get; } + + /// Index of the entry within the import section. + public uint CellIndex { get; } + + public FixupCellRef(uint tableIndex, uint cellIndex) + { + TableIndex = tableIndex; + CellIndex = cellIndex; + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeArray.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeSparseArray.cs similarity index 96% rename from src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeArray.cs rename to src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeSparseArray.cs index c8810a66476500..65a86510337f91 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeArray.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeSparseArray.cs @@ -9,7 +9,7 @@ namespace ILCompiler.Reflection.ReadyToRun /// /// based on NativeFormat::NativeArray /// - public class NativeArray + public class NativeSparseArray { private const int _blockSize = 16; @@ -18,7 +18,7 @@ public class NativeArray private uint _nElements; private byte _entryIndexSize; - public NativeArray(NativeReader reader, uint offset) + public NativeSparseArray(NativeReader reader, uint offset) { _reader = reader; diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs index 13dd20c4e979be..e42cfc288c5089 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs @@ -727,7 +727,7 @@ private void EnsureDebugInfo() int debugInfoSectionOffset = GetOffset(debugInfoSection.RelativeVirtualAddress); - NativeArray debugInfoArray = new NativeArray(ImageReader, (uint)debugInfoSectionOffset); + NativeSparseArray debugInfoArray = new NativeSparseArray(ImageReader, (uint)debugInfoSectionOffset); for (uint i = 0; i < debugInfoArray.GetCount(); ++i) { int offset = 0; @@ -849,7 +849,7 @@ private void ParseMethodDefEntrypointsSection(ReadyToRunSection section, IAssemb { int assemblyIndex = GetAssemblyIndex(section); int methodDefEntryPointsOffset = GetOffset(section.RelativeVirtualAddress); - NativeArray methodEntryPoints = new NativeArray(ImageReader, (uint)methodDefEntryPointsOffset); + NativeSparseArray methodEntryPoints = new NativeSparseArray(ImageReader, (uint)methodDefEntryPointsOffset); uint nMethodEntryPoints = methodEntryPoints.GetCount(); for (uint rid = 1; rid <= nMethodEntryPoints; rid++) @@ -882,7 +882,7 @@ private void ParseMethodDefEntrypointsSection(ReadyToRunSection section, IAssemb private void ParseMethodDefEntrypointsSectionCustom(IR2RSignatureTypeProvider provider, Dictionary foundMethods, ReadyToRunSection section, IAssemblyMetadata metadataReader) { int methodDefEntryPointsOffset = GetOffset(section.RelativeVirtualAddress); - NativeArray methodEntryPoints = new NativeArray(ImageReader, (uint)methodDefEntryPointsOffset); + NativeSparseArray methodEntryPoints = new NativeSparseArray(ImageReader, (uint)methodDefEntryPointsOffset); uint nMethodEntryPoints = methodEntryPoints.GetCount(); for (uint rid = 1; rid <= nMethodEntryPoints; rid++) From cc18125cfc3e8e6c17b84ab9524aa175ee37c442 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:25:23 -0700 Subject: [PATCH 32/74] Crossgen2 CI triage: cross-reference runtime pipeline and restrict to main branch (#37) --- .github/workflows/crossgen2-ci-triage.md | 62 ++++++++++++++++++++---- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/.github/workflows/crossgen2-ci-triage.md b/.github/workflows/crossgen2-ci-triage.md index 203153c7f2ba22..4c1ccc4855a4cd 100644 --- a/.github/workflows/crossgen2-ci-triage.md +++ b/.github/workflows/crossgen2-ci-triage.md @@ -105,9 +105,21 @@ Analyze failures from these Azure DevOps pipelines (org: `dnceng-public`, projec 3. `runtime-coreclr crossgen2 outerloop` 4. `runtime-coreclr crossgen2-composite gcstress` +### Cross-Reference Pipeline + +The following pipeline is used for cross-referencing failures (see Step 3): + +- `runtime` + +This pipeline is **not** a triage target — do not create issues for its failures. It is only queried when crossgen2 pipeline failures are found, to determine whether those failures also occur in the `runtime` pipeline (which indicates they are not crossgen2-specific). + +### Branch Restriction + +**All pipeline queries — both target pipelines and the cross-reference pipeline — MUST be filtered to the `main` branch only** (`branchName=refs/heads/main`). Do not analyze builds from pull request branches or any other branches. PR builds may have failures that will be fixed before merging to `main`. + ## Step 1: Discover Failed Builds -Query Azure DevOps for builds completed in the last 48 hours (to cover weekends on Monday) that have failures. +Query Azure DevOps for builds completed in the last 48 hours (to cover weekends on Monday) that have failures. **Only query builds on the `main` branch — never analyze PR builds.** For each target pipeline: @@ -121,11 +133,11 @@ For each target pipeline: ``` curl -s "https://dev.azure.com/dnceng-public/public/_apis/build/builds?definitions=&minTime=&resultFilter=failed&statusFilter=completed&branchName=refs/heads/main&api-version=7.0" ``` - Use the current UTC time minus 48 hours for `minTime` in ISO 8601 format. + Use the current UTC time minus 48 hours for `minTime` in ISO 8601 format. The `branchName=refs/heads/main` filter is **required** — do not omit it. 3. **Collect build IDs** for all failed builds across all four pipelines. -If no failed builds are found across any pipeline, call the `noop` safe output with a message explaining that no crossgen2 pipeline failures were found in the last 48 hours. +If no failed builds are found across any target pipeline, call the `noop` safe output with a message explaining that no crossgen2 pipeline failures were found in the last 48 hours. ## Step 2: Analyze Each Failed Build @@ -153,7 +165,33 @@ Skip failures that are already matched to known issues by Build Analysis. Focus Read from `cache-memory` a file named `triaged-builds.json` (if it exists). This contains build IDs and failure signatures that have already been triaged. Skip any failures that match entries in this file. -## Step 3: Search for Existing Issues +## Step 3: Cross-Reference Failures Against the `runtime` Pipeline + +If Step 2 identified unknown, untracked crossgen2 failures, query the `runtime` pipeline to check whether those failures also occur there. **Skip this step entirely if there are no unknown crossgen2 failures to cross-reference.** + +First, discover failed `runtime` pipeline builds using the same approach as Step 1 (same time window, same `branchName=refs/heads/main` filter): + +1. Look up the pipeline definition ID for `runtime`. +2. Query failed builds on `main` in the last 48 hours. +3. Collect the `runtime` build IDs. + +Then, for each failed `runtime` pipeline build, run the CI Analysis script: + +```bash +pwsh .github/skills/ci-analysis/scripts/Get-CIStatus.ps1 -BuildId -ShowLogs -SearchMihuBot -ContinueOnError +``` + +Extract the failing test names from the `runtime` pipeline builds. Build a set of **runtime pipeline failure signatures** (fully qualified test names). + +Then compare the crossgen2 pipeline failures (from Step 2) against the runtime pipeline failures: + +- If a test failure from a crossgen2 pipeline **also appears in the `runtime` pipeline** (matching by fully qualified test name, regardless of error category or platform), mark that failure as a **runtime-shared failure**. +- **Do NOT create issues for runtime-shared failures.** These failures are not specific to crossgen2 and do not warrant new crossgen2 issues. +- Instead, collect all runtime-shared failures to be reported in the `noop` safe output (see Step 5). + +Only failures that are **unique to the crossgen2 pipelines** (i.e., not found in the `runtime` pipeline) should proceed to Step 4 and potentially have issues created. + +## Step 4: Search for Existing Issues For each unknown failure, search GitHub for existing issues that might already track it: @@ -167,9 +205,9 @@ For each unknown failure, search GitHub for existing issues that might already t If an existing open issue already tracks the failure, skip creating a new one. Note the existing issue number in your analysis. -## Step 4: Create Issues for New Failures +## Step 5: Create Issues for New Failures -For each genuinely new, untracked failure, create a GitHub issue using the `create-issue` safe output. +For each genuinely new, untracked failure that is **unique to the crossgen2 pipelines** (not a runtime-shared failure from Step 3), create a GitHub issue using the `create-issue` safe output. ### Assess Fix Complexity @@ -262,10 +300,11 @@ For Option B (disabling tests), provide specific guidance: - Suggest the correct `[ActiveIssue]` attribute syntax - Note which configurations to disable for (e.g., only crossgen2, only specific OS) -## Step 5: Update Cache Memory +## Step 6: Update Cache Memory After processing all builds, write the updated `triaged-builds.json` to `cache-memory` with: -- Build IDs that were analyzed +- Build IDs that were analyzed from the crossgen2 target pipelines +- Build IDs from the `runtime` cross-reference pipeline that were checked (to avoid re-analyzing them) - Failure signatures (test name + error category) that were triaged - Timestamp of this triage run @@ -273,6 +312,8 @@ Use filesystem-safe timestamp format `YYYY-MM-DD-HH-MM-SS` (no colons). ## Important Guidelines +- **Only analyze `main` branch builds.** Never analyze builds from pull request branches or feature branches. PR builds may contain failures that will be fixed before merging. +- **Do not create issues for failures that also occur in the `runtime` pipeline.** These are not crossgen2-specific. Instead, note them in the `noop` safe output. - **Do not create duplicate issues.** Always search thoroughly before creating. - **Do not create issues for known/tracked failures.** If Build Analysis has already matched a failure to a known issue, skip it. - **Be conservative with "simple fix" assessments.** When in doubt, instruct Copilot to disable the test rather than attempt a fix. @@ -282,4 +323,7 @@ Use filesystem-safe timestamp format `YYYY-MM-DD-HH-MM-SS` (no colons). - Do NOT say "Helix console logs are not accessible without authentication" as a substitute for error details. The CI analysis script already extracts error information — use it. - **Include enough context in issues** for Copilot Coding Agent to act without further investigation. - **Group related failures.** If the same test fails across multiple pipelines or configurations, create a single issue covering all occurrences. -- If there are no new unknown failures to report, call the `noop` safe output explaining what you analyzed and that all failures are either known or already tracked. +- When calling the `noop` safe output, include: + - A summary of what was analyzed (which pipelines, how many builds) + - Any failures that were skipped because they also appear in the `runtime` pipeline (list the test names and note they are shared with `runtime`) + - Any failures that were skipped because they match known issues or cached entries From fa70608d80cd4e38055489e8488c04acab1e2cab Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:38:21 -0700 Subject: [PATCH 33/74] Don't use MutableModuleWrappedMethodIL for async thunks --- .../Compiler/ReadyToRunCodegenCompilation.cs | 240 +++++++++++++----- .../IL/ReadyToRunILProvider.cs | 133 ++-------- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 10 +- .../TypeSystem/Mutable/MutableModule.cs | 5 +- 4 files changed, 201 insertions(+), 187 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs index d254986e09d2a3..094271e3bb02e1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs @@ -708,11 +708,11 @@ protected override void ComputeDependencyNodeDependencies(List + { + switch(il.GetObject(tok)) + { + case string: + break; + case TypeSystemEntity tse: + EnsureDefTokensAreAvailable(tse, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module); + break; + } + return tok; + }); + var exceptionRegions = (ILExceptionRegion[])il.GetExceptionRegions(); + for (int i = 0; i < exceptionRegions.Length; i++) + { + var region = exceptionRegions[i]; + if (region.Kind == ILExceptionRegionKind.Catch) + { + TypeSystemEntity catchType = (TypeSystemEntity)il.GetObject(region.ClassToken); + EnsureDefTokensAreAvailable(catchType, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module); + } + } + } + finally + { + NodeFactory.ManifestMetadataTable._mutableModule.CreatingTokensForAsyncMethod = false; + } + } + void ProcessMutableMethodBodiesList() { MethodDesc[] mutableMethodBodyNeedList = new MethodDesc[_methodsWhichNeedMutableILBodies.Count]; @@ -933,51 +979,11 @@ private void EnsureInstantiationReferencesArePresentForExternalMethod(MethodDesc { // Validate that the typedef tokens for all of the instantiation parameters of the method // have tokens. + var moduleForNewReferences = ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module; foreach (var type in method.Instantiation) - EnsureTypeDefTokensAreReady(type); + EnsureTypeDefTokensAreAvailableInVersionBubble(type, moduleForNewReferences); foreach (var type in method.OwningType.Instantiation) - EnsureTypeDefTokensAreReady(type); - - void EnsureTypeDefTokensAreReady(TypeDesc type) - { - // Type represented by simple element type - if (type.IsPrimitive || type.IsVoid || type.IsObject || type.IsString || type.IsTypedReference) - return; - - if (type is EcmaType ecmaType) - { - if (!_nodeFactory.Resolver.GetModuleTokenForType(ecmaType, allowDynamicallyCreatedReference: false, throwIfNotFound: false).IsNull) - return; - try - { - Debug.Assert(_nodeFactory.CompilationModuleGroup.CrossModuleInlineableModule(ecmaType.Module)); - _nodeFactory.ManifestMetadataTable._mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences - = ecmaType.Module; - if (!_nodeFactory.ManifestMetadataTable._mutableModule.TryGetEntityHandle(ecmaType).HasValue) - throw new InternalCompilerErrorException($"Unable to create token to {ecmaType}"); - } - finally - { - _nodeFactory.ManifestMetadataTable._mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences - = null; - } - return; - } - - if (type.HasInstantiation) - { - EnsureTypeDefTokensAreReady(type.GetTypeDefinition()); - - foreach (TypeDesc instParam in type.Instantiation) - { - EnsureTypeDefTokensAreReady(instParam); - } - } - else if (type.IsParameterizedType) - { - EnsureTypeDefTokensAreReady(type.GetParameterType()); - } - } + EnsureTypeDefTokensAreAvailableInVersionBubble(type, moduleForNewReferences); } private void AddNecessaryAsyncReferences(MethodDesc method) @@ -1012,31 +1018,141 @@ private void AddNecessaryAsyncReferences(MethodDesc method) asyncHelpers.GetKnownMethod("AllocContinuationClass"u8, null), asyncHelpers.GetKnownMethod("AllocContinuationMethod"u8, null), ]; - _nodeFactory.ManifestMetadataTable._mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module; + var moduleForNewReferences = ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module; + EnsureDefTokensAreAvailable(requiredMethods, moduleForNewReferences); + EnsureDefTokensAreAvailable(requiredTypes, moduleForNewReferences); + EnsureDefTokensAreAvailable(requiredFields, moduleForNewReferences); + _hasAddedAsyncReferences = true; + } + + void EnsureDefTokensAreAvailable(IEnumerable entities, ModuleDesc moduleForNewReferences) + { + foreach (var entity in entities) + { + EnsureDefTokensAreAvailable(entity, moduleForNewReferences); + } + } + + void EnsureDefTokensAreAvailable(TypeSystemEntity entity, ModuleDesc moduleForNewReferences) + { + switch (entity) + { + case TypeDesc typeDesc: + EnsureTypeDefTokensAreAvailableInVersionBubble(typeDesc, moduleForNewReferences); + break; + case MethodDesc methodDesc: + EnsureMethodDefTokensAreAvailableInVersionBubble(methodDesc, moduleForNewReferences); + break; + case FieldDesc fieldDesc: + EnsureFieldDefTokensAreAvailableInVersionBubble(fieldDesc, moduleForNewReferences); + break; + default: + throw new NotSupportedException(); + }; + } + + private void AddTokenToMutableModule(TypeSystemEntity entity, ModuleDesc module) + { + var existingToken = entity switch + { + TypeDesc typeDesc => _nodeFactory.Resolver.GetModuleTokenForType(typeDesc, allowDynamicallyCreatedReference: true, throwIfNotFound: false), + MethodDesc methodDesc => _nodeFactory.Resolver.GetModuleTokenForMethod(methodDesc, allowDynamicallyCreatedReference: true, throwIfNotFound: false), + FieldDesc fieldDesc => _nodeFactory.Resolver.GetModuleTokenForField(fieldDesc, allowDynamicallyCreatedReference: true, throwIfNotFound: false), + _ => throw new NotSupportedException() + }; + + var declaringModule = entity switch + { + TypeDesc ecmaType => (ecmaType.GetTypeDefinition() as EcmaType)?.Module, + MethodDesc ecmaMethod => ((EcmaMethod)ecmaMethod.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, + EcmaField ecmaField => ecmaField.Module, + _ => null + }; + + if (!existingToken.IsNull) + return; + try { - // Unconditionally add references to the MutableModule. These members are internal / private and - // shouldn't be referenced already, and this lets us avoid doing this more than once - foreach (var td in requiredTypes) + _nodeFactory.ManifestMetadataTable._mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = module; + if (!_nodeFactory.ManifestMetadataTable._mutableModule.TryGetEntityHandle(entity).HasValue) { - if (!_nodeFactory.ManifestMetadataTable._mutableModule.TryGetEntityHandle(td).HasValue) - throw new InternalCompilerErrorException($"Unable to create token to {td}"); + throw new InternalCompilerErrorException($"Unable to create token to {entity}"); } - foreach (var fd in requiredFields) + } + finally + { + _nodeFactory.ManifestMetadataTable._mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = null; + } + } + + private void EnsureMethodDefTokensAreAvailableInVersionBubble(MethodDesc methodDesc, ModuleDesc moduleForNewReferences) + { + if (!methodDesc.IsPrimaryMethodDesc()) + { + EnsureMethodDefTokensAreAvailableInVersionBubble(methodDesc.GetPrimaryMethodDesc(), moduleForNewReferences); + return; + } + if (methodDesc is EcmaMethod ecmaMethod) + { + AddTokenToMutableModule(ecmaMethod, ecmaMethod.Module); + return; + } + if (methodDesc.HasInstantiation) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(methodDesc.GetMethodDefinition().OwningType, moduleForNewReferences); + foreach (TypeDesc instParam in methodDesc.Instantiation) { - if (!_nodeFactory.ManifestMetadataTable._mutableModule.TryGetEntityHandle(fd).HasValue) - throw new InternalCompilerErrorException($"Unable to create token to {fd}"); + EnsureTypeDefTokensAreAvailableInVersionBubble(instParam, moduleForNewReferences); } - foreach (var md in requiredMethods) + EnsureMethodDefTokensAreAvailableInVersionBubble(methodDesc.GetTypicalMethodDefinition(), moduleForNewReferences); + } + } + + private void EnsureFieldDefTokensAreAvailableInVersionBubble(FieldDesc fieldDesc, ModuleDesc moduleForNewReferences) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(fieldDesc.OwningType, moduleForNewReferences); + EnsureTypeDefTokensAreAvailableInVersionBubble(fieldDesc.FieldType, moduleForNewReferences); + if (fieldDesc is EcmaField ecmaField) + { + AddTokenToMutableModule(ecmaField, ecmaField.Module); + } + else + { + EnsureFieldDefTokensAreAvailableInVersionBubble(fieldDesc.GetTypicalFieldDefinition(), moduleForNewReferences); + } + } + + private void EnsureTypeDefTokensAreAvailableInVersionBubble(TypeDesc type, ModuleDesc moduleForNewReferences) + { + // Type represented by simple element type + if (type.IsPrimitive || type.IsVoid || type.IsObject || type.IsString || type.IsTypedReference) + return; + + if (type is EcmaType ecmaType) + { + AddTokenToMutableModule(ecmaType, ecmaType.Module); + return; + } + if (type is ParameterizedType parameterizedType) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(parameterizedType.ParameterType, moduleForNewReferences); + AddTokenToMutableModule(parameterizedType, moduleForNewReferences); + return; + } + + if (type.HasInstantiation) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(type.GetTypeDefinition(), moduleForNewReferences); + + foreach (TypeDesc instParam in type.Instantiation) { - if (!_nodeFactory.ManifestMetadataTable._mutableModule.TryGetEntityHandle(md).HasValue) - throw new InternalCompilerErrorException($"Unable to create token to {md}"); + EnsureTypeDefTokensAreAvailableInVersionBubble(instParam, moduleForNewReferences); } - _hasAddedAsyncReferences = true; } - finally + else if (type.IsParameterizedType) { - _nodeFactory.ManifestMetadataTable._mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = null; + EnsureTypeDefTokensAreAvailableInVersionBubble(type.GetParameterType(), moduleForNewReferences); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs index 5460cad265c0d6..8e7e1bbd939c3f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs @@ -15,6 +15,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using ILCompiler.ReadyToRun.TypeSystem; +using ILCompiler.DependencyAnalysis.ReadyToRun; namespace Internal.IL { @@ -135,57 +136,15 @@ public void CreateCrossModuleInlineableTokensForILBody(MethodDesc method) Debug.Assert(_manifestMutableModule != null); var wrappedMethodIL = new ManifestModuleWrappedMethodIL(); - if (method.IsAsync) - { - Debug.Assert(NeedsTaskReturningThunk(method)); - if (!wrappedMethodIL.Initialize(_manifestMutableModule, GetMethodILForAsyncMethod(method), (EcmaMethod)method, false)) - { - // If we could not initialize the wrapped method IL, we should store a null. - // That will result in the IL code for the method being unavailable for use in - // the compilation, which is version safe. - wrappedMethodIL = null; - } - } - else if (method.IsAsyncVariant()) - { - Debug.Assert(NeedsAsyncThunk(method)); - if (!wrappedMethodIL.Initialize(_manifestMutableModule, - AsyncThunkILEmitter.EmitAsyncMethodThunk(method, method.GetTargetOfAsyncVariant()), - method, - false)) - { - // If we could not initialize the wrapped method IL, we should store a null. - // That will result in the IL code for the method being unavailable for use in - // the compilation, which is version safe. - wrappedMethodIL = null; - } - } - else if (method is AsyncResumptionStub ars) - { - if (!wrappedMethodIL.Initialize( - _manifestMutableModule, - ars.EmitIL(), - ars, - false)) - { - // If we could not initialize the wrapped method IL, we should store a null. - // That will result in the IL code for the method being unavailable for use in - // the compilation, which is version safe. - wrappedMethodIL = null; - } - } - else - { - Debug.Assert(!_compilationModuleGroup.VersionsWithMethodBody(method) && - _compilationModuleGroup.CrossModuleInlineable(method)); + Debug.Assert(!_compilationModuleGroup.VersionsWithMethodBody(method) && + _compilationModuleGroup.CrossModuleInlineable(method)); - if (!wrappedMethodIL.Initialize(_manifestMutableModule, EcmaMethodIL.Create((EcmaMethod)method))) - { - // If we could not initialize the wrapped method IL, we should store a null. - // That will result in the IL code for the method being unavailable for use in - // the compilation, which is version safe. - wrappedMethodIL = null; - } + if (!wrappedMethodIL.Initialize(_manifestMutableModule, EcmaMethodIL.Create((EcmaMethod)method))) + { + // If we could not initialize the wrapped method IL, we should store a null. + // That will result in the IL code for the method being unavailable for use in + // the compilation, which is version safe. + wrappedMethodIL = null; } _manifestModuleWrappedMethods.Add(method, wrappedMethodIL); @@ -196,9 +155,9 @@ public bool NeedsCrossModuleInlineableTokens(MethodDesc method) { bool regularCrossModuleInlineable = (!_compilationModuleGroup.VersionsWithMethodBody(method) && _compilationModuleGroup.CrossModuleInlineable(method)); - bool requiredCrossModuleInliningForAsync = (NeedsTaskReturningThunk(method) || NeedsAsyncThunk(method) || method is AsyncResumptionStub) - && !_compilationModuleGroup.VersionsWithModule(method.Context.SystemModule); - if ((regularCrossModuleInlineable || requiredCrossModuleInliningForAsync) + // bool requiredCrossModuleInliningForAsync = (NeedsTaskReturningThunk(method) || NeedsAsyncThunk(method) || method is AsyncResumptionStub) + // && !_compilationModuleGroup.VersionsWithModule(method.Context.SystemModule); + if ((regularCrossModuleInlineable) // || requiredCrossModuleInliningForAsync) && !_manifestModuleWrappedMethods.ContainsKey(method)) { return true; @@ -206,7 +165,7 @@ public bool NeedsCrossModuleInlineableTokens(MethodDesc method) return false; } - bool NeedsTaskReturningThunk(MethodDesc method) + public static bool NeedsTaskReturningThunk(MethodDesc method) { Debug.Assert(method.IsTypicalMethodDefinition); if (method is not EcmaMethod ecmaMethod) @@ -224,7 +183,7 @@ bool NeedsTaskReturningThunk(MethodDesc method) return false; } - bool NeedsAsyncThunk(MethodDesc method) + public static bool NeedsAsyncThunk(MethodDesc method) { Debug.Assert(method.IsTypicalMethodDefinition); if (method is not AsyncMethodVariant) @@ -232,7 +191,7 @@ bool NeedsAsyncThunk(MethodDesc method) return !method.IsAsync; } - MethodIL GetMethodILForAsyncMethod(MethodDesc method) + private MethodIL GetMethodILForAsyncMethod(MethodDesc method) { Debug.Assert(method.IsAsync && method is EcmaMethod); if (method.Signature.ReturnsTaskOrValueTask()) @@ -295,8 +254,6 @@ public override MethodIL GetMethodIL(MethodDesc method) } else if (method is AsyncResumptionStub ars) { - if (_manifestModuleWrappedMethods.TryGetValue(ars, out var methodil)) - return methodil; return ars.EmitIL(); } else @@ -317,7 +274,6 @@ class ManifestModuleWrappedMethodIL : MethodIL, IEcmaMethodIL, IMethodTokensAreU ILExceptionRegion[] _exceptionRegions; byte[] _ilBytes; LocalVariableDefinition[] _locals; - HashSet _methodsWithAsyncVariants; MutableModule _mutableModule; @@ -325,20 +281,10 @@ public ManifestModuleWrappedMethodIL() {} public bool Initialize(MutableModule mutableModule, EcmaMethodIL wrappedMethod) { - return Initialize(mutableModule, wrappedMethod, wrappedMethod.OwningMethod, true); - } - - public bool Initialize(MutableModule mutableModule, MethodIL wrappedMethod, MethodDesc owningMethod, bool validateStandaloneMetadata) - { - HashSet methodsWhichCannotHaveAsyncVariants = null; - _methodsWithAsyncVariants = null; - - if (wrappedMethod == null) - return false; - bool failedToReplaceToken = false; try { + var owningMethod = wrappedMethod.OwningMethod; Debug.Assert(mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences == null); mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = ((EcmaMethod)owningMethod.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module; var owningMethodHandle = mutableModule.TryGetEntityHandle(owningMethod); @@ -368,8 +314,7 @@ public bool Initialize(MutableModule mutableModule, MethodIL wrappedMethod, Meth ILTokenReplacer.Replace(_ilBytes, GetMutableModuleToken); #if DEBUG - if (validateStandaloneMetadata) - Debug.Assert(ReadyToRunStandaloneMethodMetadata.Compute((EcmaMethod)_owningMethod.GetPrimaryMethodDesc().GetTypicalMethodDefinition()) != null); + Debug.Assert(ReadyToRunStandaloneMethodMetadata.Compute((EcmaMethod)_owningMethod.GetPrimaryMethodDesc().GetTypicalMethodDefinition()) != null); #endif // DEBUG } finally @@ -390,41 +335,6 @@ int GetMutableModuleToken(int token) } else { - // Since async thunks directly refer to async methods(which is otherwise not permitted in IL), we need to track this detail - // when we replace the tokens, and use tokens for the non-async variant method, but return - // the async variant as appropriate. - if (result is MethodDesc methodDesc) - { - if (methodDesc.IsAsyncVariant()) - { - // We actually need to store the non-variant method, and force GetObject - // to return the async variant - methodDesc = methodDesc.GetTargetOfAsyncVariant(); - if (_methodsWithAsyncVariants == null) - _methodsWithAsyncVariants = new HashSet(); - _methodsWithAsyncVariants.Add(methodDesc); - result = methodDesc; - - if (methodsWhichCannotHaveAsyncVariants != null && - methodsWhichCannotHaveAsyncVariants.Contains(methodDesc)) - { - // This method cannot refer to both an async thunk and async variant, fail the compile - throw new Exception("Method refers in IL directly to an async variant method and a non-async variant"); - } - } - else if (methodDesc.IsAsync) - { - if (methodsWhichCannotHaveAsyncVariants == null) - methodsWhichCannotHaveAsyncVariants = new HashSet(); - methodsWhichCannotHaveAsyncVariants.Add(methodDesc); - if (_methodsWithAsyncVariants != null && - _methodsWithAsyncVariants.Contains(methodDesc)) - { - // This method cannot refer to both an async thunk and async variant, fail the compile - throw new Exception("Method refers in IL directly to an async variant method and a non-async variant"); - } - } - } newToken = mutableModule.TryGetHandle((TypeSystemEntity)result); } if (!newToken.HasValue) @@ -454,14 +364,7 @@ public override object GetObject(int token, NotFoundBehavior notFoundBehavior = if ((token & 0xFF000000) == 0x70000000) return _mutableModule.GetUserString(System.Reflection.Metadata.Ecma335.MetadataTokens.UserStringHandle(token)); - object result = _mutableModule.GetObject(System.Reflection.Metadata.Ecma335.MetadataTokens.EntityHandle(token), notFoundBehavior); - if (_methodsWithAsyncVariants != null && - _methodsWithAsyncVariants.Contains(result)) - { - // Return the async variant method - result = ((MethodDesc)result).GetAsyncVariant(); - } - return result; + return _mutableModule.GetObject(System.Reflection.Metadata.Ecma335.MetadataTokens.EntityHandle(token), notFoundBehavior); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index cb670ba2db0cbe..2d32a997d9b535 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -564,15 +564,7 @@ public static bool ShouldSkipCompilation(InstructionSetSupport instructionSetSup // Special methods on delegate types return true; } - // Async resumption stubs use faux IL with synthetic tokens. When CoreLib is in the - // version bubble the stubs are not wrapped with ManifestModuleWrappedMethodIL, so - // token resolution for InstantiatedType / ParameterizedType falls through to a path - // that cannot handle them. Skip compilation and let the runtime JIT these stubs. - // https://github.com/dotnet/runtime/issues/125337 - if (methodNeedingCode.IsCompilerGeneratedILBodyForAsync() && compilation != null && compilation.NodeFactory.CompilationModuleGroup.IsCompositeBuildMode) - { - return true; - } + if (ShouldCodeNotBeCompiledIntoFinalImage(instructionSetSupport, methodNeedingCode)) { return true; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/TypeSystem/Mutable/MutableModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/TypeSystem/Mutable/MutableModule.cs index 79c0853b0d817e..e571ed1d8b3ddc 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/TypeSystem/Mutable/MutableModule.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/TypeSystem/Mutable/MutableModule.cs @@ -39,7 +39,8 @@ protected override EntityHandle GetNonNestedResolutionScope(MetadataType metadat if (!_mutableModule._moduleToModuleRefString.TryGetValue(module, out moduleRefString)) { Debug.Assert(_mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences != null && - _mutableModule._compilationGroup.CrossModuleInlineableModule(_mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences)); + (_mutableModule._compilationGroup.CrossModuleInlineableModule(_mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences) + || _mutableModule.CreatingTokensForAsyncMethod)); if (module == _typeSystemContext.SystemModule) { @@ -353,6 +354,8 @@ private int GetAssemblyRefHandle(TypeSystemMetadataEmitter emitter, object name) public int ModuleTypeSort => 1; + public bool CreatingTokensForAsyncMethod { get; set; } + public int CompareTo(IEcmaModule other) { if (other == this) From 8dc7ab03f4ab750681815f8b2de16833636a5979 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:15:17 -0700 Subject: [PATCH 34/74] Force OwningTypeNotDerivedFromToken for method references in async thunks In "fauxMethodIL", we resolve tokens for methods and fields as the method/field definition, and then emit them as signatures with the instantiations encoded in the signature. However, the OwningType in MethodWithToken isn't properly computed when the token doesn't resolve to the instantiated MemberRef / MethodSpec. To account for this, force the OwningTypeNotDerivedFromToken property to be set in MethodWithToken when we strip an instantiation from the methodDesc to acquire the token. --- .../tools/Common/JitInterface/CorInfoImpl.cs | 7 +- .../ReadyToRun/ILBodyFixupSignature.cs | 2 +- .../ReadyToRun/InstanceEntryPointTableNode.cs | 2 +- .../InstrumentationDataTableNode.cs | 2 +- .../ReadyToRunCodegenNodeFactory.cs | 2 +- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 128 +++++++++++------- 6 files changed, 86 insertions(+), 57 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index 1654ad2613be04..b5bf8ff3c821bf 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -1873,6 +1873,7 @@ private void resolveToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken) else { recordToken = (_compilation.CompilationModuleGroup.VersionsWithType(owningType) || _compilation.CompilationModuleGroup.CrossModuleInlineableType(owningType)) && owningType is EcmaType; + recordToken &= !methodIL.OwningMethod.IsCompilerGeneratedILBodyForAsync(); } #endif @@ -1885,7 +1886,8 @@ private void resolveToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken) #if READYTORUN if (recordToken) { - ModuleToken methodModuleToken = HandleToModuleToken(ref pResolvedToken); + ModuleToken methodModuleToken = HandleToModuleToken(ref pResolvedToken, out bool strippedInstantiation); + Debug.Assert(!strippedInstantiation); var resolver = _compilation.NodeFactory.Resolver; resolver.AddModuleTokenForMethod(method, methodModuleToken); ValidateSafetyOfUsingTypeEquivalenceInSignature(method.Signature); @@ -1952,7 +1954,8 @@ private void resolveToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken) #if READYTORUN if (recordToken) { - _compilation.NodeFactory.Resolver.AddModuleTokenForType(type, HandleToModuleToken(ref pResolvedToken)); + _compilation.NodeFactory.Resolver.AddModuleTokenForType(type, HandleToModuleToken(ref pResolvedToken, out bool strippedInstantiation)); + Debug.Assert(!strippedInstantiation); } #endif diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ILBodyFixupSignature.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ILBodyFixupSignature.cs index 45b39e8de77d72..92f21a58cbf73e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ILBodyFixupSignature.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ILBodyFixupSignature.cs @@ -77,7 +77,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) dataBuilder.EmitTypeSignature(typeRef, innerContext); } - MethodWithToken method = new MethodWithToken(_method, moduleToken, null, unboxing: false, context: null); + MethodWithToken method = new MethodWithToken(_method, moduleToken, null, unboxing: false, genericContextObject: null); dataBuilder.EmitMethodSignature(method, enforceDefEncoding: false, enforceOwningType: false, innerContext, false); } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstanceEntryPointTableNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstanceEntryPointTableNode.cs index ddbf57a2731e14..3a829774df604d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstanceEntryPointTableNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstanceEntryPointTableNode.cs @@ -73,7 +73,7 @@ public static byte[] BuildSignatureForMethodDefinedInModule(MethodDesc method, N ArraySignatureBuilder signatureBuilder = new ArraySignatureBuilder(); signatureBuilder.EmitMethodSignature( - new MethodWithToken(method, moduleToken, constrainedType: null, unboxing: false, context: null), + new MethodWithToken(method, moduleToken, constrainedType: null, unboxing: false, genericContextObject: null), enforceDefEncoding: true, enforceOwningType: moduleToken.Module is EcmaModule ? factory.CompilationModuleGroup.EnforceOwningType((EcmaModule)moduleToken.Module) : true, factory.SignatureContext, diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstrumentationDataTableNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstrumentationDataTableNode.cs index 323661407e311d..9a152a33a61df2 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstrumentationDataTableNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstrumentationDataTableNode.cs @@ -147,7 +147,7 @@ private int MethodToInt(TypeSystemEntityOrUnknown handle) EcmaMethod typicalMethod = (EcmaMethod)handle.AsMethod.GetTypicalMethodDefinition(); ModuleToken moduleToken = new ModuleToken(typicalMethod.Module, typicalMethod.Handle); - MethodWithToken tok = new MethodWithToken(handle.AsMethod, moduleToken, constrainedType: null, unboxing: false, context: null); + MethodWithToken tok = new MethodWithToken(handle.AsMethod, moduleToken, constrainedType: null, unboxing: false, genericContextObject: null); Import methodHandleImport = (Import)_symbolFactory.CreateReadyToRunHelper(ReadyToRunHelperId.MethodHandle, tok); _imports.Add(methodHandleImport); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index 88278fc10f707f..fb2ef7b40b959d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -503,7 +503,7 @@ public IEnumerable EnumerateCompiledMethods(EcmaModule moduleT EcmaModule module = ((EcmaMethod)method.GetTypicalMethodDefinition()).Module; ModuleToken moduleToken = Resolver.GetModuleTokenForMethod(method, allowDynamicallyCreatedReference: true, throwIfNotFound: true); - IMethodNode methodNodeDebug = MethodEntrypoint(new MethodWithToken(method, moduleToken, constrainedType: null, unboxing: false, context: null), false, false, false); + IMethodNode methodNodeDebug = MethodEntrypoint(new MethodWithToken(method, moduleToken, constrainedType: null, unboxing: false, genericContextObject: null), false, false, false); MethodWithGCInfo methodCodeNodeDebug = methodNodeDebug as MethodWithGCInfo; if (methodCodeNodeDebug == null && methodNodeDebug is DelayLoadMethodImport DelayLoadMethodImport) { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 2d32a997d9b535..11ab3d19cf59d3 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -135,17 +135,26 @@ public class MethodWithToken public readonly bool OwningTypeNotDerivedFromToken; public readonly TypeDesc OwningType; - public MethodWithToken(MethodDesc method, ModuleToken token, TypeDesc constrainedType, bool unboxing, TypeSystemEntity context, TypeDesc devirtualizedMethodOwner = null) + public MethodWithToken(MethodDesc method, ModuleToken token, TypeDesc constrainedType, bool unboxing, TypeSystemEntity genericContextObject, TypeDesc devirtualizedMethodOwner = null, bool forceOwningTypeFromMethodDesc = false) { Debug.Assert(!method.IsUnboxingThunk()); + Debug.Assert(genericContextObject is null or MethodDesc or TypeDesc); Method = method; Token = token; ConstrainedType = constrainedType; Unboxing = unboxing; - OwningType = GetMethodTokenOwningType(this, constrainedType, context, devirtualizedMethodOwner, out OwningTypeNotDerivedFromToken); + if (!forceOwningTypeFromMethodDesc) + { + OwningType = GetMethodTokenOwningType(this, constrainedType, genericContextObject, devirtualizedMethodOwner, out OwningTypeNotDerivedFromToken); + } + else + { + OwningType = method.OwningType; + OwningTypeNotDerivedFromToken = true; + } } - private static TypeDesc GetMethodTokenOwningType(MethodWithToken methodToken, TypeDesc constrainedType, TypeSystemEntity context, TypeDesc devirtualizedMethodOwner, out bool owningTypeNotDerivedFromToken) + private static TypeDesc GetMethodTokenOwningType(MethodWithToken methodToken, TypeDesc constrainedType, TypeSystemEntity genericContextObject, TypeDesc devirtualizedMethodOwner, out bool owningTypeNotDerivedFromToken) { ModuleToken moduleToken = methodToken.Token; owningTypeNotDerivedFromToken = false; @@ -160,7 +169,7 @@ private static TypeDesc GetMethodTokenOwningType(MethodWithToken methodToken, Ty if (moduleToken.TokenType == CorTokenType.mdtMethodDef) { var methodDefinition = moduleToken.MetadataReader.GetMethodDefinition((MethodDefinitionHandle)moduleToken.Handle); - return HandleContext(moduleToken.Module, methodDefinition.GetDeclaringType(), methodToken.Method.OwningType, constrainedType, context, devirtualizedMethodOwner, ref owningTypeNotDerivedFromToken); + return HandleContext(moduleToken.Module, methodDefinition.GetDeclaringType(), methodToken.Method.OwningType, constrainedType, genericContextObject, devirtualizedMethodOwner, ref owningTypeNotDerivedFromToken); } // At this point moduleToken must point at a MemberRef. @@ -173,19 +182,19 @@ private static TypeDesc GetMethodTokenOwningType(MethodWithToken methodToken, Ty case HandleKind.TypeSpecification: { Debug.Assert(devirtualizedMethodOwner == null); // Devirtualization is expected to always use a methoddef token - return HandleContext(moduleToken.Module, memberRef.Parent, methodToken.Method.OwningType, constrainedType, context, null, ref owningTypeNotDerivedFromToken); + return HandleContext(moduleToken.Module, memberRef.Parent, methodToken.Method.OwningType, constrainedType, genericContextObject, null, ref owningTypeNotDerivedFromToken); } default: return methodToken.Method.OwningType; } - TypeDesc HandleContext(IEcmaModule module, EntityHandle handle, TypeDesc methodTargetOwner, TypeDesc constrainedType, TypeSystemEntity context, TypeDesc devirtualizedMethodOwner, ref bool owningTypeNotDerivedFromToken) + static TypeDesc HandleContext(IEcmaModule module, EntityHandle handle, TypeDesc methodTargetOwner, TypeDesc constrainedType, TypeSystemEntity genericContextObject, TypeDesc devirtualizedMethodOwner, ref bool owningTypeNotDerivedFromToken) { var tokenOnlyOwningType = module.GetType(handle); TypeDesc actualOwningType; - if (context == null) + if (genericContextObject == null) { actualOwningType = methodTargetOwner; } @@ -194,14 +203,14 @@ TypeDesc HandleContext(IEcmaModule module, EntityHandle handle, TypeDesc methodT Instantiation typeInstantiation; Instantiation methodInstantiation = new Instantiation(); - if (context is MethodDesc methodContext) + if (genericContextObject is MethodDesc methodContext) { typeInstantiation = methodContext.OwningType.Instantiation; methodInstantiation = methodContext.Instantiation; } else { - TypeDesc typeContext = (TypeDesc)context; + TypeDesc typeContext = (TypeDesc)genericContextObject; typeInstantiation = typeContext.Instantiation; } @@ -323,12 +332,8 @@ public bool Equals(MethodWithToken methodWithToken) bool equals = Method == methodWithToken.Method && Token.Equals(methodWithToken.Token) && OwningType == methodWithToken.OwningType && ConstrainedType == methodWithToken.ConstrainedType - && Unboxing == methodWithToken.Unboxing; - if (equals) - { - Debug.Assert(OwningTypeNotDerivedFromToken == methodWithToken.OwningTypeNotDerivedFromToken); - Debug.Assert(OwningType == methodWithToken.OwningType); - } + && Unboxing == methodWithToken.Unboxing + && OwningTypeNotDerivedFromToken == methodWithToken.OwningTypeNotDerivedFromToken && OwningType == methodWithToken.OwningType; return equals; } @@ -405,8 +410,12 @@ public int CompareTo(MethodWithToken other, TypeSystemComparer comparer) return result; // The OwningType/OwningTypeNotDerivedFromToken should be equivalent if the above conditions are equal. - Debug.Assert(OwningTypeNotDerivedFromToken == other.OwningTypeNotDerivedFromToken); - Debug.Assert(OwningTypeNotDerivedFromToken || (OwningType == other.OwningType)); + result = OwningTypeNotDerivedFromToken.CompareTo(other.OwningTypeNotDerivedFromToken); + if (result != 0) + return result; + result = comparer.Compare(OwningType, other.OwningType); + if (result != 0) + return result; if (OwningTypeNotDerivedFromToken != other.OwningTypeNotDerivedFromToken) { @@ -961,10 +970,11 @@ private void getReadyToRunDelegateCtorHelper(ref CORINFO_RESOLVED_TOKEN pTargetM MethodWithToken targetMethod = new MethodWithToken( targetMethodDesc, - HandleToModuleToken(ref pTargetMethod), + HandleToModuleToken(ref pTargetMethod, out bool strippedInstantiation), constrainedType: constrainedType, unboxing: false, - context: typeOrMethodContext); + genericContextObject: typeOrMethodContext, + forceOwningTypeFromMethodDesc: strippedInstantiation); // runtime lookup is not needed, callerHandle is unused pLookup.lookupKind.needsRuntimeLookup = false; @@ -1372,13 +1382,13 @@ private bool canTailCall(CORINFO_METHOD_STRUCT_* callerHnd, CORINFO_METHOD_STRUC private FieldWithToken ComputeFieldWithToken(FieldDesc field, ref CORINFO_RESOLVED_TOKEN pResolvedToken) { - ModuleToken token = HandleToModuleToken(ref pResolvedToken); + ModuleToken token = HandleToModuleToken(ref pResolvedToken, out _); return new FieldWithToken(field, token); } private MethodWithToken ComputeMethodWithToken(MethodDesc method, ref CORINFO_RESOLVED_TOKEN pResolvedToken, TypeDesc constrainedType, bool unboxing) { - ModuleToken token = HandleToModuleToken(ref pResolvedToken, method, out TypeSystemEntity context, ref constrainedType); + ModuleToken token = _HandleToModuleToken(ref pResolvedToken, method, out TypeSystemEntity context, ref constrainedType, out bool strippedInstantiation); TypeDesc devirtualizedMethodOwner = null; if (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_DevirtualizedMethod) @@ -1386,42 +1396,48 @@ private MethodWithToken ComputeMethodWithToken(MethodDesc method, ref CORINFO_RE devirtualizedMethodOwner = HandleToObject(pResolvedToken.hClass); } - return new MethodWithToken(method, token, constrainedType: constrainedType, unboxing: unboxing, context: context, devirtualizedMethodOwner: devirtualizedMethodOwner); - } + // For crossgen-generated il stubs, we may only have access to the method token for the typical method defition. + // This means that the owning type of the method may not be encoded in the method token. + // bool methodTokenMightNotEncodeOwningType = MethodBeingCompiled.IsCompilerGeneratedILBodyForAsync(); + return new MethodWithToken(method, token, constrainedType: constrainedType, unboxing: unboxing, genericContextObject: context, devirtualizedMethodOwner: devirtualizedMethodOwner, forceOwningTypeFromMethodDesc: strippedInstantiation); - private ModuleToken HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken, MethodDesc methodDesc, out TypeSystemEntity context, ref TypeDesc constrainedType) - { - if (methodDesc != null && (_compilation.NodeFactory.CompilationModuleGroup.VersionsWithMethodBody(methodDesc) - || (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_DevirtualizedMethod) - || methodDesc.IsPInvoke)) + ModuleToken _HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken, MethodDesc methodDesc, out TypeSystemEntity context, ref TypeDesc constrainedType, out bool strippedInstantiation) { - if ((CorTokenType)(unchecked((uint)pResolvedToken.token) & 0xFF000000u) == CorTokenType.mdtMethodDef && - methodDesc?.GetTypicalMethodDefinition() is EcmaMethod ecmaMethod) + if (methodDesc != null && (_compilation.NodeFactory.CompilationModuleGroup.VersionsWithMethodBody(methodDesc) + || (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_DevirtualizedMethod) + || methodDesc.IsPInvoke)) { - mdToken token = (mdToken)MetadataTokens.GetToken(ecmaMethod.Handle); + if ((CorTokenType)(unchecked((uint)pResolvedToken.token) & 0xFF000000u) == CorTokenType.mdtMethodDef && + methodDesc?.GetTypicalMethodDefinition() is EcmaMethod ecmaMethod) + { + mdToken token = (mdToken)MetadataTokens.GetToken(ecmaMethod.Handle); - // This is used for de-virtualization of non-generic virtual methods, and should be treated - // as a the methodDesc parameter defining the exact OwningType, not doing resolution through the token. - context = null; - constrainedType = null; + // This is used for de-virtualization of non-generic virtual methods, and should be treated + // as a the methodDesc parameter defining the exact OwningType, not doing resolution through the token. + context = null; + constrainedType = null; + strippedInstantiation = false; - return new ModuleToken(ecmaMethod.Module, token); + return new ModuleToken(ecmaMethod.Module, token); + } } - } - if (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_ResolvedStaticVirtualMethod) - { - context = null; - } - else - { - context = entityFromContext(pResolvedToken.tokenContext); - } + if (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_ResolvedStaticVirtualMethod) + { + context = null; + } + else + { + context = entityFromContext(pResolvedToken.tokenContext); + } - return HandleToModuleToken(ref pResolvedToken); + var x = HandleToModuleToken(ref pResolvedToken, out bool s); + strippedInstantiation = s; + return x; + } } - private ModuleToken HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken) + private ModuleToken HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken, out bool strippedInstantiation) { mdToken token = pResolvedToken.token; var methodIL = HandleToObject(pResolvedToken.tokenScope); @@ -1448,7 +1464,11 @@ private ModuleToken HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToke // It's okay to strip the instantiation away because we don't need a MethodSpec // token - SignatureBuilder will generate the generic method signature // using instantiation parameters from the MethodDesc entity. - resultMethod = resultMethod.GetPrimaryMethodDesc().GetTypicalMethodDefinition(); + // However, we need to make note of this to indicate that the OwningType of the + // method should be inferred from the MethodDesc entity rather than the token. + var primaryMethod = resultMethod.GetPrimaryMethodDesc(); + resultMethod = primaryMethod.GetTypicalMethodDefinition(); + strippedInstantiation = resultMethod != primaryMethod; if (!_compilation.NodeFactory.CompilationModuleGroup.VersionsWithType(resultMethod.OwningType)) { @@ -1465,6 +1485,7 @@ private ModuleToken HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToke // instantiated MemberRef token - SignatureBuilder will generate the generic // field signature using instantiation parameters from the FieldDesc entity. resultField = resultField.GetTypicalFieldDefinition(); + strippedInstantiation = resultField != resultDef; Debug.Assert(resultField is EcmaField); Debug.Assert(_compilation.NodeFactory.CompilationModuleGroup.VersionsWithType(resultField.OwningType) || resultField.OwningType.IsNonVersionable()); @@ -1473,11 +1494,14 @@ private ModuleToken HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToke } else { + // We don't strip the instantiation for instantiated types in GetModuleTokenForType + strippedInstantiation = false; return GetModuleTokenForType((TypeSystemEntity)resultDef); } } else { + strippedInstantiation = false; module = ((IEcmaMethodIL)methodILDef).Module; } @@ -2731,12 +2755,14 @@ private void ComputeRuntimeLookupForSharedGenericToken( { var methodIL = HandleToObject(pResolvedToken.tokenScope); MethodDesc sharedMethod = methodIL.OwningMethod.GetSharedRuntimeFormMethodTarget(); + // We shouldn't be needing shared generics in resumption stubs - generics info should all be stored in the continuation + Debug.Assert(MethodBeingCompiled is not AsyncResumptionStub); _compilation.NodeFactory.DetectGenericCycles(MethodBeingCompiled, sharedMethod); - helperArg = new MethodWithToken(methodDesc, HandleToModuleToken(ref pResolvedToken), constrainedType, unboxing: false, context: sharedMethod); + helperArg = new MethodWithToken(methodDesc, HandleToModuleToken(ref pResolvedToken, out bool strippedInstantiation), constrainedType, unboxing: false, genericContextObject: sharedMethod, forceOwningTypeFromMethodDesc: strippedInstantiation); } else if (helperArg is FieldDesc fieldDesc) { - helperArg = new FieldWithToken(fieldDesc, HandleToModuleToken(ref pResolvedToken)); + helperArg = new FieldWithToken(fieldDesc, HandleToModuleToken(ref pResolvedToken, out _)); } var methodContext = new GenericContext(callerHandle); @@ -3159,7 +3185,7 @@ private void getAddressOfPInvokeTarget(CORINFO_METHOD_STRUCT_* method, ref CORIN Debug.Assert(_compilation.CompilationModuleGroup.VersionsWithMethodBody(methodDesc)); EcmaMethod ecmaMethod = (EcmaMethod)methodDesc; ModuleToken moduleToken = new ModuleToken(ecmaMethod.Module, ecmaMethod.Handle); - MethodWithToken methodWithToken = new MethodWithToken(ecmaMethod, moduleToken, constrainedType: null, unboxing: false, context: null); + MethodWithToken methodWithToken = new MethodWithToken(ecmaMethod, moduleToken, constrainedType: null, unboxing: false, genericContextObject: null); if ((ecmaMethod.GetPInvokeMethodCallingConventions() & UnmanagedCallingConventions.IsSuppressGcTransition) != 0) { From 849c73b19db243272f875db95c64aba536024551 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:15:37 -0700 Subject: [PATCH 35/74] Revert NativeArray changes --- .../MethodDefEntryPointsTable.cs | 2 +- .../NativeArray.cs | 114 ++++++++++++++++++ .../ReadyToRunReader.cs | 6 +- 3 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeArray.cs diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs index 5d924111f08934..fc5c2209dadb56 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs @@ -28,7 +28,7 @@ private MethodDefEntryPointsTable(List entries) public static MethodDefEntryPointsTable Parse(ReadyToRunReader reader, ReadyToRunSection section) { int sectionOffset = reader.GetOffset(section.RelativeVirtualAddress); - NativeSparseArray methodEntryPoints = new NativeSparseArray(reader.ImageReader, (uint)sectionOffset); + NativeArray methodEntryPoints = new NativeArray(reader.ImageReader, (uint)sectionOffset); uint count = methodEntryPoints.GetCount(); var entries = new List((int)count); diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeArray.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeArray.cs new file mode 100644 index 00000000000000..c8810a66476500 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeArray.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Text; + +namespace ILCompiler.Reflection.ReadyToRun +{ + /// + /// based on NativeFormat::NativeArray + /// + public class NativeArray + { + private const int _blockSize = 16; + + private NativeReader _reader; + private uint _baseOffset; + private uint _nElements; + private byte _entryIndexSize; + + public NativeArray(NativeReader reader, uint offset) + { + _reader = reader; + + uint val = 0; + _baseOffset = _reader.DecodeUnsigned(offset, ref val); + _nElements = (val >> 2); + _entryIndexSize = (byte)(val & 3); + } + + public uint GetCount() + { + return _nElements; + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + sb.AppendLine($"NativeArray Size: {_nElements}"); + sb.AppendLine($"EntryIndexSize: {_entryIndexSize}"); + for (uint i = 0; i < _nElements; i++) + { + int val = 0; + if (TryGetAt(i, ref val)) + { + sb.AppendLine($"{i}: {val}"); + } + } + + return sb.ToString(); + } + + public bool TryGetAt(uint index, ref int pOffset) + { + if (index >= _nElements) + return false; + + uint offset; + if (_entryIndexSize == 0) + { + int i = (int)(_baseOffset + (index / _blockSize)); + offset = _reader.ReadByte(ref i); + } + else if (_entryIndexSize == 1) + { + int i = (int)(_baseOffset + 2 * (index / _blockSize)); + offset = _reader.ReadUInt16(ref i); + } + else + { + int i = (int)(_baseOffset + 4 * (index / _blockSize)); + offset = _reader.ReadUInt32(ref i); + } + offset += _baseOffset; + + for (uint bit = _blockSize >> 1; bit > 0; bit >>= 1) + { + uint val = 0; + uint offset2 = _reader.DecodeUnsigned(offset, ref val); + if ((index & bit) != 0) + { + if ((val & 2) != 0) + { + offset += val >> 2; + continue; + } + } + else + { + if ((val & 1) != 0) + { + offset = offset2; + continue; + } + } + + // Not found + if ((val & 3) == 0) + { + // Matching special leaf node? + if ((val >> 2) == (index & (_blockSize - 1))) + { + offset = offset2; + break; + } + } + return false; + } + pOffset = (int)offset; + return true; + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs index e42cfc288c5089..13dd20c4e979be 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs @@ -727,7 +727,7 @@ private void EnsureDebugInfo() int debugInfoSectionOffset = GetOffset(debugInfoSection.RelativeVirtualAddress); - NativeSparseArray debugInfoArray = new NativeSparseArray(ImageReader, (uint)debugInfoSectionOffset); + NativeArray debugInfoArray = new NativeArray(ImageReader, (uint)debugInfoSectionOffset); for (uint i = 0; i < debugInfoArray.GetCount(); ++i) { int offset = 0; @@ -849,7 +849,7 @@ private void ParseMethodDefEntrypointsSection(ReadyToRunSection section, IAssemb { int assemblyIndex = GetAssemblyIndex(section); int methodDefEntryPointsOffset = GetOffset(section.RelativeVirtualAddress); - NativeSparseArray methodEntryPoints = new NativeSparseArray(ImageReader, (uint)methodDefEntryPointsOffset); + NativeArray methodEntryPoints = new NativeArray(ImageReader, (uint)methodDefEntryPointsOffset); uint nMethodEntryPoints = methodEntryPoints.GetCount(); for (uint rid = 1; rid <= nMethodEntryPoints; rid++) @@ -882,7 +882,7 @@ private void ParseMethodDefEntrypointsSection(ReadyToRunSection section, IAssemb private void ParseMethodDefEntrypointsSectionCustom(IR2RSignatureTypeProvider provider, Dictionary foundMethods, ReadyToRunSection section, IAssemblyMetadata metadataReader) { int methodDefEntryPointsOffset = GetOffset(section.RelativeVirtualAddress); - NativeSparseArray methodEntryPoints = new NativeSparseArray(ImageReader, (uint)methodDefEntryPointsOffset); + NativeArray methodEntryPoints = new NativeArray(ImageReader, (uint)methodDefEntryPointsOffset); uint nMethodEntryPoints = methodEntryPoints.GetCount(); for (uint rid = 1; rid <= nMethodEntryPoints; rid++) From b53da9f717419c31b3182b95e66f9146bfdfe188 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:47:05 -0700 Subject: [PATCH 36/74] Undo all changes related to the test infra change this was branched off --- eng/Subsets.props | 2 - .../ILCompiler.ReadyToRun.Tests.csproj | 62 -- .../CrossModuleInlining/AsyncMethods.cs | 27 - .../CrossModuleInlining/BasicInlining.cs | 26 - .../CrossModuleInlining/CompositeAsync.cs | 35 - .../CrossModuleInlining/CompositeBasic.cs | 20 - .../Dependencies/AsyncCompositeLib.cs | 25 - .../Dependencies/AsyncInlineableLib.cs | 15 - .../Dependencies/CompositeLib.cs | 11 - .../Dependencies/CrossModuleGenericLib.cs | 39 - .../Dependencies/ExternalLib.cs | 19 - .../Dependencies/InlineableLib.cs | 14 - .../Dependencies/InlineableLibTransitive.cs | 14 - .../Dependencies/MultiStepLibA.cs | 13 - .../Dependencies/MultiStepLibB.cs | 13 - .../MultiInlinerConsumer.cs | 28 - .../CrossModuleInlining/MultiStepConsumer.cs | 20 - .../TransitiveReferences.cs | 26 - .../TestCases/R2RTestSuites.cs | 955 ------------------ .../RuntimeAsync/AsyncCrossModule.cs | 28 - .../AsyncCrossModuleContinuation.cs | 21 - .../RuntimeAsync/AsyncDevirtualize.cs | 56 - .../TestCases/RuntimeAsync/AsyncNoYield.cs | 23 - .../RuntimeAsync/AsyncTransitiveMain.cs | 21 - .../RuntimeAsync/AsyncWithContinuation.cs | 26 - .../RuntimeAsync/BasicAsyncEmission.cs | 36 - .../RuntimeAsync/CompositeAsyncDevirtMain.cs | 22 - .../RuntimeAsync/Dependencies/AsyncDepLib.cs | 28 - .../Dependencies/AsyncDepLibContinuation.cs | 27 - .../Dependencies/AsyncExternalLib.cs | 13 - .../Dependencies/AsyncInterfaceLib.cs | 30 - .../Dependencies/AsyncTransitiveLib.cs | 23 - .../RuntimeAsyncMethodGenerationAttribute.cs | 10 - .../TestCasesRunner/R2RDriver.cs | 145 --- .../TestCasesRunner/R2RResultChecker.cs | 434 -------- .../TestCasesRunner/R2RTestCaseCompiler.cs | 129 --- .../TestCasesRunner/R2RTestRunner.cs | 410 -------- .../TestCasesRunner/TestPaths.cs | 192 ---- src/coreclr/tools/aot/crossgen2.slnx | 3 - src/coreclr/tools/r2rdump/TextDumper.cs | 8 +- 40 files changed, 2 insertions(+), 3047 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs diff --git a/eng/Subsets.props b/eng/Subsets.props index ef81a5c2c1a8a3..5ce04cb6645861 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -502,8 +502,6 @@ Test="true" Category="clr" Condition="'$(DotNetBuildSourceOnly)' != 'true' and '$(NativeAotSupported)' == 'true'"/> - diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj deleted file mode 100644 index 9398f04c97b171..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj +++ /dev/null @@ -1,62 +0,0 @@ - - - - ILCompiler.ReadyToRun.Tests - $(NetCoreAppToolCurrent) - enable - false - true - x64;x86 - AnyCPU - linux-x64;win-x64;osx-x64 - Debug;Release;Checked - true - -notrait category=failing - - - - - - - - - - - - - - - - - - - - - - $(RuntimeBinDir)/crossgen2 - - - $(MicrosoftNetCoreAppRuntimePackRidLibTfmDir) - - - $(MicrosoftNetCoreAppRefPackRefDir) - - - $(CoreCLRArtifactsPath) - - - $(NetCoreAppCurrentTestHostSharedFrameworkPath) - - - $(TargetArchitecture) - - - $(TargetOS) - - - $(Configuration) - - - - diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs deleted file mode 100644 index fdf1b159130b95..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Test: Cross-module inlining of async methods -// Validates that async methods from AsyncInlineableLib are cross-module -// inlined into this assembly with CHECK_IL_BODY fixups. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncMethods -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task TestAsyncInline() - { - return await AsyncInlineableLib.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task TestAsyncStringInline() - { - return await AsyncInlineableLib.GetStringAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static int TestSyncFromAsyncLib() - { - return AsyncInlineableLib.GetValueSync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs deleted file mode 100644 index ba301272868c25..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Test: Basic cross-module inlining -// Validates that crossgen2 with --opt-cross-module produces CHECK_IL_BODY fixups -// for methods inlined from InlineableLib into this main assembly. -using System; -using System.Runtime.CompilerServices; - -public static class BasicInlining -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static int TestGetValue() - { - return InlineableLib.GetValue(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static string TestGetString() - { - return InlineableLib.GetString(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static int TestAdd() - { - return InlineableLib.Add(10, 32); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs deleted file mode 100644 index f315a93d6269b9..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Test: Composite mode with runtime-async methods across assemblies. -// Validates that async methods produce [ASYNC] variants in composite output. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class CompositeAsyncMain -{ - public static int Main() - { - int sync = CallCompositeSync(); - if (sync != 99) - return 1; - - return 0; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCompositeAsync() - { - return await AsyncCompositeLib.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCompositeStringAsync() - { - return await AsyncCompositeLib.GetStringAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static int CallCompositeSync() - { - return AsyncCompositeLib.GetValueSync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs deleted file mode 100644 index 0ba24d5563555c..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Test: Composite mode basic compilation -// Validates that composite mode R2R compilation with multiple assemblies -// produces correct manifest references and component assembly entries. -using System; -using System.Runtime.CompilerServices; - -public static class CompositeBasic -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static int TestCompositeCall() - { - return CompositeLib.GetCompositeValue(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static object TestCompositeTypeCreation() - { - return new CompositeLib.CompositeType(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs deleted file mode 100644 index bece15b74b11b6..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Dependency library for composite async tests. -// Contains runtime-async methods called from another assembly in composite mode. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncCompositeLib -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetValueAsync() - { - await Task.Yield(); - return 42; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetStringAsync() - { - await Task.Yield(); - return "composite_async"; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetValueSync() => 99; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs deleted file mode 100644 index 153804e6279fc1..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncInlineableLib -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetValueAsync() => 42; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetStringAsync() => "Hello from async"; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetValueSync() => 42; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs deleted file mode 100644 index 2dc5db2de38bcc..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -public static class CompositeLib -{ - public static int GetCompositeValue() => 100; - - public class CompositeType - { - public string Name { get; set; } = "Composite"; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs deleted file mode 100644 index b17643fb9eb99b..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Runtime.CompilerServices; - -/// -/// Library with two generic types that each inline the same utility method. -/// When compiled via CrossModuleCompileable generics, each type's InvokeGetValue() -/// becomes a distinct cross-module inliner MethodDef for the same inlinee (Utility.GetValue), -/// producing multiple cross-module inliner entries in the CrossModuleInlineInfo section. -/// -public static class Utility -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetValue() => 42; -} - -public class GenericWrapperA -{ - private T _value; - - public GenericWrapperA(T value) => _value = value; - - [MethodImpl(MethodImplOptions.NoInlining)] - public virtual int InvokeGetValue() - { - return Utility.GetValue(); - } -} - -public class GenericWrapperB -{ - private T _value; - - public GenericWrapperB(T value) => _value = value; - - [MethodImpl(MethodImplOptions.NoInlining)] - public virtual int InvokeGetValue() - { - return Utility.GetValue(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs deleted file mode 100644 index d56f2880564a13..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -public static class ExternalLib -{ - public static int ExternalValue => 99; - - public class ExternalType - { - public int Value { get; set; } - } - - public class Outer - { - public class Inner - { - public static int NestedValue => 77; - } - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs deleted file mode 100644 index a799cfed7282e8..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -public static class InlineableLib -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetValue() => 42; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetString() => "Hello from InlineableLib"; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int Add(int a, int b) => a + b; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs deleted file mode 100644 index 15fd29dda19d4b..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -public static class InlineableLibTransitive -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetExternalValue() => ExternalLib.ExternalValue; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetNestedValue() => ExternalLib.Outer.Inner.NestedValue; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExternalLib.ExternalType CreateExternal() => new ExternalLib.ExternalType { Value = 42 }; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs deleted file mode 100644 index c47dd6ac274f28..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Dependency library for multi-step compilation tests. -// Contains sync inlineable methods used in both composite and non-composite steps. -using System; -using System.Runtime.CompilerServices; - -public static class MultiStepLibA -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetValue() => 42; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetLabel() => "LibA"; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs deleted file mode 100644 index cf8acd54d8bd9b..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Second library for multi-step composite compilation. -// Compiled together with MultiStepLibA as a composite in step 1. -using System; -using System.Runtime.CompilerServices; - -public static class MultiStepLibB -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetCompositeValue() => MultiStepLibA.GetValue() + 1; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetCompositeLabel() => MultiStepLibA.GetLabel() + "_B"; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs deleted file mode 100644 index 56e9c37d33b7f5..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs +++ /dev/null @@ -1,28 +0,0 @@ -/// -/// Consumer that uses two different generic types from CrossModuleGenericLib, -/// each instantiated with a value type defined in this assembly. -/// Value types are required because CrossModuleCompileable discovery uses -/// CanonicalFormKind.Specific, which preserves value type arguments (unlike -/// reference types which become __Canon, losing the alternate location info). -/// -/// GenericWrapperA<LocalStruct>.InvokeGetValue() and GenericWrapperB<LocalStruct>.InvokeGetValue() -/// are two distinct MethodDefs that each inline Utility.GetValue(), producing -/// multiple cross-module inliner entries for the same inlinee in CrossModuleInlineInfo. -/// - -public struct LocalStruct { public int Value; } - -public static class MultiInlinerConsumer -{ - public static int UseA() - { - var wrapper = new GenericWrapperA(new LocalStruct { Value = 1 }); - return wrapper.InvokeGetValue(); - } - - public static int UseB() - { - var wrapper = new GenericWrapperB(new LocalStruct { Value = 2 }); - return wrapper.InvokeGetValue(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs deleted file mode 100644 index ec1264f22f1380..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Test: Non-composite consumer of assemblies that were also compiled as composite. -// Step 1 compiles MultiStepLibA + MultiStepLibB as composite. -// Step 2 compiles this assembly non-composite with --ref to LibA and --opt-cross-module. -using System; -using System.Runtime.CompilerServices; - -public static class MultiStepConsumer -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static int GetValueFromLibA() - { - return MultiStepLibA.GetValue(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static string GetLabelFromLibA() - { - return MultiStepLibA.GetLabel(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs deleted file mode 100644 index 25bac820fc3002..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Test: Transitive cross-module references -// Validates that when InlineableLibTransitive is inlined, its references to ExternalLib -// are properly encoded in the R2R image (requiring tokens for both libraries). -using System; -using System.Runtime.CompilerServices; - -public static class TransitiveReferences -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static int TestTransitiveValue() - { - return InlineableLibTransitive.GetExternalValue(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static int TestNestedTypeAccess() - { - return InlineableLibTransitive.GetNestedValue(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static object TestTransitiveTypeCreation() - { - return InlineableLibTransitive.CreateExternal(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs deleted file mode 100644 index 7388cb5d23fc7b..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ /dev/null @@ -1,955 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using ILCompiler.ReadyToRun.Tests.TestCasesRunner; -using ILCompiler.Reflection.ReadyToRun; -using Microsoft.CodeAnalysis; -using Microsoft.DotNet.XUnitExtensions; -using Xunit; -using Xunit.Abstractions; - -namespace ILCompiler.ReadyToRun.Tests.TestCases; - -/// -/// xUnit test suites for R2R cross-module resolution tests. -/// Each test method builds assemblies with Roslyn, crossgen2's them, and validates the R2R output. -/// -public class R2RTestSuites -{ - private static readonly KeyValuePair RuntimeAsyncFeature = new("runtime-async", "on"); - private readonly ITestOutputHelper _output; - - public R2RTestSuites(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void BasicCrossModuleInlining() - { - var InlineableLib = new CompiledAssembly - { - AssemblyName = "InlineableLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], - }; - var basicCrossModuleInlining = new CompiledAssembly - { - AssemblyName = "BasicCrossModuleInlining", - SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], - References = [InlineableLib] - }; - - var cgInlineableLib = new CrossgenAssembly(InlineableLib){ Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization] }; - var cgBasicCrossModuleInlining = new CrossgenAssembly(basicCrossModuleInlining); - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(BasicCrossModuleInlining), - [new CrossgenCompilation(basicCrossModuleInlining.AssemblyName, [cgInlineableLib, cgBasicCrossModuleInlining]) { Validate = Validate }]) - ); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "InlineableLib"); - R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetValue", "GetValue"); - R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetString", "GetString"); - R2RAssert.HasCrossModuleInliningInfo(reader); - } - } - - [Fact] - public void TransitiveReferences() - { - var externalLib = new CompiledAssembly() - { - AssemblyName = "ExternalLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/ExternalLib.cs"], - }; - var inlineableLibTransitive = new CompiledAssembly() - { - AssemblyName = "InlineableLibTransitive", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLibTransitive.cs"], - References = [externalLib] - }; - var transitiveReferences = new CompiledAssembly() - { - AssemblyName = "TransitiveReferences", - SourceResourceNames = ["CrossModuleInlining/TransitiveReferences.cs"], - References = [inlineableLibTransitive, externalLib] - }; - new R2RTestRunner(_output).Run(new R2RTestCase(nameof(TransitiveReferences), - [ - new("TransitiveReferences", [ - new CrossgenAssembly(transitiveReferences), - new CrossgenAssembly(externalLib) { Kind = Crossgen2InputKind.Reference }, - new CrossgenAssembly(inlineableLibTransitive) - { - Kind = Crossgen2InputKind.Reference, - Options = [Crossgen2AssemblyOption.CrossModuleOptimization], - }, - ]) - { - Validate = reader => - { - R2RAssert.HasManifestRef(reader, "InlineableLibTransitive"); - R2RAssert.HasManifestRef(reader, "ExternalLib"); - R2RAssert.HasCrossModuleInlinedMethod(reader, "TestTransitiveValue", "GetExternalValue"); - }, - }, - ])); - } - - [Fact] - public void AsyncCrossModuleInlining() - { - var asyncInlineableLib = new CompiledAssembly - { - AssemblyName = "AsyncInlineableLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncInlineableLib.cs"], - }; - var asyncCrossModuleInlining = new CompiledAssembly - { - AssemblyName = nameof(AsyncCrossModuleInlining), - SourceResourceNames = ["CrossModuleInlining/AsyncMethods.cs"], - References = [asyncInlineableLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(AsyncCrossModuleInlining), - [ - new(nameof(AsyncCrossModuleInlining), - [ - new CrossgenAssembly(asyncCrossModuleInlining), - new CrossgenAssembly(asyncInlineableLib) - { - Kind = Crossgen2InputKind.Reference, - Options = [Crossgen2AssemblyOption.CrossModuleOptimization], - }, - ]) - { - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "AsyncInlineableLib"); - R2RAssert.HasCrossModuleInlinedMethod(reader, "TestAsyncInline", "GetValueAsync"); - } - } - - [Fact] - public void CompositeBasic() - { - var compositeLib = new CompiledAssembly - { - AssemblyName = "CompositeLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/CompositeLib.cs"], - }; - var compositeBasic = new CompiledAssembly - { - AssemblyName = nameof(CompositeBasic), - SourceResourceNames = ["CrossModuleInlining/CompositeBasic.cs"], - References = [compositeLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(CompositeBasic), - [ - new(nameof(CompositeBasic), - [ - new CrossgenAssembly(compositeLib), - new CrossgenAssembly(compositeBasic), - ]) - { - Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "CompositeLib"); - } - } - - [Fact] - public void RuntimeAsyncMethodEmission() - { - var runtimeAsyncMethodEmission = new CompiledAssembly - { - AssemblyName = nameof(RuntimeAsyncMethodEmission), - SourceResourceNames = - [ - "RuntimeAsync/BasicAsyncEmission.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(RuntimeAsyncMethodEmission), - [ - new(nameof(RuntimeAsyncMethodEmission), [new CrossgenAssembly(runtimeAsyncMethodEmission)]) - { - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasAsyncVariant(reader, "SimpleAsyncMethod"); - R2RAssert.HasAsyncVariant(reader, "AsyncVoidReturn"); - R2RAssert.HasAsyncVariant(reader, "ValueTaskMethod"); - } - } - - /// - /// PR #123643: Async methods capturing GC refs across await points - /// produce ContinuationLayout fixups encoding the GC ref map. - /// PR #124203: Resumption stubs for methods with suspension points. - /// - [Fact] - public void RuntimeAsyncContinuationLayout() - { - var runtimeAsyncContinuationLayout = new CompiledAssembly - { - AssemblyName = nameof(RuntimeAsyncContinuationLayout), - SourceResourceNames = - [ - "RuntimeAsync/AsyncWithContinuation.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(RuntimeAsyncContinuationLayout), - [ - new(nameof(RuntimeAsyncContinuationLayout), [new CrossgenAssembly(runtimeAsyncContinuationLayout)]) - { - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasAsyncVariant(reader, "CaptureObjectAcrossAwait"); - R2RAssert.HasAsyncVariant(reader, "CaptureMultipleRefsAcrossAwait"); - R2RAssert.HasContinuationLayout(reader, "CaptureObjectAcrossAwait"); - R2RAssert.HasContinuationLayout(reader, "CaptureMultipleRefsAcrossAwait"); - R2RAssert.HasResumptionStubFixup(reader, "CaptureObjectAcrossAwait"); - } - } - - /// - /// PR #125420: [ASYNC] variant generation for devirtualizable async call patterns - /// (sealed class and interface dispatch through AsyncAwareVirtualMethodResolutionAlgorithm). - /// - [Fact] - public void RuntimeAsyncDevirtualize() - { - var runtimeAsyncDevirtualize = new CompiledAssembly - { - AssemblyName = nameof(RuntimeAsyncDevirtualize), - SourceResourceNames = - [ - "RuntimeAsync/AsyncDevirtualize.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(RuntimeAsyncDevirtualize), - [ - new(nameof(RuntimeAsyncDevirtualize), [new CrossgenAssembly(runtimeAsyncDevirtualize)]) - { - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasAsyncVariant(reader, "GetValueAsync"); - } - } - - /// - /// PR #124203: Async methods without yield points may omit resumption stubs. - /// Validates that no-yield async methods still produce [ASYNC] variants. - /// - [Fact] - public void RuntimeAsyncNoYield() - { - var runtimeAsyncNoYield = new CompiledAssembly - { - AssemblyName = nameof(RuntimeAsyncNoYield), - SourceResourceNames = - [ - "RuntimeAsync/AsyncNoYield.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(RuntimeAsyncNoYield), - [ - new(nameof(RuntimeAsyncNoYield), [new CrossgenAssembly(runtimeAsyncNoYield)]) - { - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasAsyncVariant(reader, "AsyncButNoAwait"); - R2RAssert.HasAsyncVariant(reader, "AsyncWithConditionalAwait"); - } - } - - /// - /// PR #121679: MutableModule async references + cross-module inlining - /// of runtime-async methods with cross-module dependency. - /// - [Fact] - public void RuntimeAsyncCrossModule() - { - var asyncDepLib = new CompiledAssembly - { - AssemblyName = "AsyncDepLib", - SourceResourceNames = - [ - "RuntimeAsync/Dependencies/AsyncDepLib.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - }; - var runtimeAsyncCrossModule = new CompiledAssembly - { - AssemblyName = nameof(RuntimeAsyncCrossModule), - SourceResourceNames = - [ - "RuntimeAsync/AsyncCrossModule.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - References = [asyncDepLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(RuntimeAsyncCrossModule), - [ - new(nameof(RuntimeAsyncCrossModule), - [ - new CrossgenAssembly(runtimeAsyncCrossModule), - new CrossgenAssembly(asyncDepLib) - { - Kind = Crossgen2InputKind.Reference, - Options = [Crossgen2AssemblyOption.CrossModuleOptimization], - }, - ]) - { - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "AsyncDepLib"); - R2RAssert.HasAsyncVariant(reader, "CallCrossModuleAsync"); - } - } - - // ===================================================================== - // Tier 1: Critical intersection tests - // ===================================================================== - - /// - /// Composite mode with sync cross-module inlining. - /// Validates that inlining info (CrossModuleInlineInfo or InliningInfo2) is - /// properly populated (CompositeBasic only validates ManifestRef). - /// - [Fact] - public void CompositeCrossModuleInlining() - { - var inlineableLib = new CompiledAssembly - { - AssemblyName = "InlineableLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], - }; - var compositeMain = new CompiledAssembly - { - AssemblyName = "CompositeCrossModuleInlining", - SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], - References = [inlineableLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(CompositeCrossModuleInlining), - [ - new(nameof(CompositeCrossModuleInlining), - [ - new CrossgenAssembly(inlineableLib), - new CrossgenAssembly(compositeMain), - ]) - { - Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "InlineableLib"); - R2RAssert.HasInlinedMethod(reader, "TestGetValue", "GetValue"); - } - } - - /// - /// Composite mode with runtime-async methods in both assemblies. - /// Validates async variants exist in composite output. - /// - [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] - [Fact] - public void CompositeAsync() - { - var asyncCompositeLib = new CompiledAssembly - { - AssemblyName = "AsyncCompositeLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], - Features = { RuntimeAsyncFeature }, - }; - var compositeAsyncMain = new CompiledAssembly - { - AssemblyName = "CompositeAsyncMain", - SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], - Features = { RuntimeAsyncFeature }, - References = [asyncCompositeLib], - OutputKind = OutputKind.ConsoleApplication - }; - - var compositeAsyncMainCG2 = new CrossgenAssembly(compositeAsyncMain); - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(CompositeAsync), - [ - new(nameof(CompositeAsync), - [ - new CrossgenAssembly(asyncCompositeLib), - compositeAsyncMainCG2 - ]) - { - Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], - Validate = Validate, - Execute = compositeAsyncMainCG2, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); - R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); - R2RAssert.HasAsyncVariant(reader, "GetValueAsync"); - } - } - - /// - /// The full intersection: composite + runtime-async + cross-module inlining. - /// Async methods from AsyncCompositeLib are inlined into CompositeAsyncMain - /// within a composite image, exercising MutableModule token encoding for - /// cross-module async continuation layouts. - /// - [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] - [Fact] - public void CompositeAsyncCrossModuleInlining() - { - var asyncCompositeLib = new CompiledAssembly - { - AssemblyName = "AsyncCompositeLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], - Features = { RuntimeAsyncFeature }, - }; - var compositeAsyncMain = new CompiledAssembly - { - AssemblyName = "CompositeAsyncMain", - SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], - Features = { RuntimeAsyncFeature }, - References = [asyncCompositeLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(CompositeAsyncCrossModuleInlining), - [ - new(nameof(CompositeAsyncCrossModuleInlining), - [ - new CrossgenAssembly(asyncCompositeLib), - new CrossgenAssembly(compositeAsyncMain), - ]) - { - Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); - R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); - R2RAssert.HasInlinedMethod(reader, "CallCompositeAsync", "GetValueAsync"); - R2RAssert.HasContinuationLayout(reader, "CallCompositeAsync"); - } - } - - /// - /// Non-composite runtime-async + cross-module inlining where the inlinee - /// captures GC refs across await points. Validates that ContinuationLayout - /// fixups correctly reference cross-module types via MutableModule tokens. - /// - [Fact] - public void AsyncCrossModuleContinuation() - { - var asyncDepLibCont = new CompiledAssembly - { - AssemblyName = "AsyncDepLibContinuation", - SourceResourceNames = - [ - "RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - }; - var asyncCrossModuleCont = new CompiledAssembly - { - AssemblyName = nameof(AsyncCrossModuleContinuation), - SourceResourceNames = - [ - "RuntimeAsync/AsyncCrossModuleContinuation.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - References = [asyncDepLibCont] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(AsyncCrossModuleContinuation), - [ - new(nameof(AsyncCrossModuleContinuation), - [ - new CrossgenAssembly(asyncCrossModuleCont), - new CrossgenAssembly(asyncDepLibCont) - { - Kind = Crossgen2InputKind.Reference, - Options = [Crossgen2AssemblyOption.CrossModuleOptimization], - }, - ]) - { - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation"); - R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef"); - R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureArray"); - } - } - - /// - /// Two-step compilation: composite A+B, then non-composite C referencing A. - /// Exercises the multi-compilation model. - /// - [Fact] - public void MultiStepCompositeAndNonComposite() - { - var libA = new CompiledAssembly - { - AssemblyName = "MultiStepLibA", - SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepLibA.cs"], - }; - var libB = new CompiledAssembly - { - AssemblyName = "MultiStepLibB", - SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepLibB.cs"], - References = [libA] - }; - var consumer = new CompiledAssembly - { - AssemblyName = "MultiStepConsumer", - SourceResourceNames = ["CrossModuleInlining/MultiStepConsumer.cs"], - References = [libA] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(MultiStepCompositeAndNonComposite), - [ - new("CompositeStep", - [ - new CrossgenAssembly(libA), - new CrossgenAssembly(libB), - ]) - { - Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], - Validate = reader => - { - R2RAssert.HasManifestRef(reader, "MultiStepLibA"); - }, - }, - new("NonCompositeStep", - [ - new CrossgenAssembly(consumer), - new CrossgenAssembly(libA) - { - Kind = Crossgen2InputKind.Reference, - Options = [Crossgen2AssemblyOption.CrossModuleOptimization], - }, - ]) - { - Validate = reader => - { - R2RAssert.HasManifestRef(reader, "MultiStepLibA"); - R2RAssert.HasCrossModuleInlinedMethod(reader, "GetValueFromLibA", "GetValue"); - }, - }, - ])); - } - - // ===================================================================== - // Tier 2: Depth coverage - // ===================================================================== - - /// - /// Composite + runtime-async + cross-module devirtualization. - /// Interface defined in AsyncInterfaceLib, call sites in CompositeAsyncDevirtMain. - /// - [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] - [Fact] - public void CompositeAsyncDevirtualize() - { - var asyncInterfaceLib = new CompiledAssembly - { - AssemblyName = "AsyncInterfaceLib", - SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncInterfaceLib.cs"], - Features = { RuntimeAsyncFeature }, - }; - var compositeDevirtMain = new CompiledAssembly - { - AssemblyName = "CompositeAsyncDevirtMain", - SourceResourceNames = - [ - "RuntimeAsync/CompositeAsyncDevirtMain.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - References = [asyncInterfaceLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(CompositeAsyncDevirtualize), - [ - new(nameof(CompositeAsyncDevirtualize), - [ - new CrossgenAssembly(asyncInterfaceLib), - new CrossgenAssembly(compositeDevirtMain), - ]) - { - Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "AsyncInterfaceLib"); - R2RAssert.HasAsyncVariant(reader, "CallOnSealed"); - } - } - - /// - /// Composite with 3 assemblies in A→B→C transitive chain. - /// Validates manifest refs for all three and transitive inlining. - /// - [Fact] - public void CompositeTransitive() - { - var externalLib = new CompiledAssembly - { - AssemblyName = "ExternalLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/ExternalLib.cs"], - }; - var inlineableLibTransitive = new CompiledAssembly - { - AssemblyName = "InlineableLibTransitive", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLibTransitive.cs"], - References = [externalLib] - }; - var compositeTransitiveMain = new CompiledAssembly - { - AssemblyName = "CompositeTransitive", - SourceResourceNames = ["CrossModuleInlining/TransitiveReferences.cs"], - References = [inlineableLibTransitive, externalLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(CompositeTransitive), - [ - new(nameof(CompositeTransitive), - [ - new CrossgenAssembly(externalLib), - new CrossgenAssembly(inlineableLibTransitive), - new CrossgenAssembly(compositeTransitiveMain), - ]) - { - Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "InlineableLibTransitive"); - R2RAssert.HasManifestRef(reader, "ExternalLib"); - } - } - - /// - /// Non-composite runtime-async + transitive cross-module inlining. - /// Chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. - /// - [Fact] - public void AsyncCrossModuleTransitive() - { - var asyncExternalLib = new CompiledAssembly - { - AssemblyName = "AsyncExternalLib", - SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncExternalLib.cs"], - }; - var asyncTransitiveLib = new CompiledAssembly - { - AssemblyName = "AsyncTransitiveLib", - SourceResourceNames = - [ - "RuntimeAsync/Dependencies/AsyncTransitiveLib.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - References = [asyncExternalLib] - }; - var asyncTransitiveMain = new CompiledAssembly - { - AssemblyName = nameof(AsyncCrossModuleTransitive), - SourceResourceNames = - [ - "RuntimeAsync/AsyncTransitiveMain.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - References = [asyncTransitiveLib, asyncExternalLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(AsyncCrossModuleTransitive), - [ - new(nameof(AsyncCrossModuleTransitive), - [ - new CrossgenAssembly(asyncTransitiveMain), - new CrossgenAssembly(asyncExternalLib) { Kind = Crossgen2InputKind.Reference }, - new CrossgenAssembly(asyncTransitiveLib) - { - Kind = Crossgen2InputKind.Reference, - Options = [Crossgen2AssemblyOption.CrossModuleOptimization], - }, - ]) - { - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib"); - R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync"); - } - } - - /// - /// Composite + runtime-async + transitive (3 assemblies). - /// Full combination of composite, async, and transitive references. - /// - [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] - [Fact] - public void CompositeAsyncTransitive() - { - var asyncExternalLib = new CompiledAssembly - { - AssemblyName = "AsyncExternalLib", - SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncExternalLib.cs"], - }; - var asyncTransitiveLib = new CompiledAssembly - { - AssemblyName = "AsyncTransitiveLib", - SourceResourceNames = - [ - "RuntimeAsync/Dependencies/AsyncTransitiveLib.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - References = [asyncExternalLib] - }; - var compositeAsyncTransitiveMain = new CompiledAssembly - { - AssemblyName = "CompositeAsyncTransitive", - SourceResourceNames = - [ - "RuntimeAsync/AsyncTransitiveMain.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - References = [asyncTransitiveLib, asyncExternalLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(CompositeAsyncTransitive), - [ - new(nameof(CompositeAsyncTransitive), - [ - new CrossgenAssembly(asyncExternalLib), - new CrossgenAssembly(asyncTransitiveLib), - new CrossgenAssembly(compositeAsyncTransitiveMain), - ]) - { - Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib"); - R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync"); - } - } - - /// - /// Multi-step compilation with runtime-async in all assemblies. - /// Step 1: Composite of async libs. Step 2: Non-composite consumer - /// with cross-module inlining of async methods. - /// - [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] - [Fact] - public void MultiStepCompositeAndNonCompositeAsync() - { - var asyncCompositeLib = new CompiledAssembly - { - AssemblyName = "AsyncCompositeLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], - Features = { RuntimeAsyncFeature }, - }; - var compositeAsyncMain = new CompiledAssembly - { - AssemblyName = "CompositeAsyncMain", - SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], - Features = { RuntimeAsyncFeature }, - References = [asyncCompositeLib] - }; - var asyncConsumer = new CompiledAssembly - { - AssemblyName = "MultiStepAsyncConsumer", - SourceResourceNames = - [ - "RuntimeAsync/AsyncCrossModuleContinuation.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - References = [asyncCompositeLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(MultiStepCompositeAndNonCompositeAsync), - [ - new("CompositeAsyncStep", - [ - new CrossgenAssembly(asyncCompositeLib), - new CrossgenAssembly(compositeAsyncMain), - ]) - { - Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], - Validate = reader => - { - R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); - R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); - }, - }, - new("NonCompositeAsyncStep", - [ - new CrossgenAssembly(asyncConsumer), - new CrossgenAssembly(asyncCompositeLib) - { - Kind = Crossgen2InputKind.Reference, - Options = [Crossgen2AssemblyOption.CrossModuleOptimization], - }, - ]) - { - Validate = reader => - { - R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); - R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef"); - }, - }, - ])); - } - - /// - /// Tests cross-module generic compilation where multiple generic instantiations - /// from an --opt-cross-module library each inline the same utility method. - /// This produces multiple cross-module inliners for the same inlinee in the - /// CrossModuleInlineInfo section, exercising the absolute-index encoding - /// (not delta-encoded) for cross-module inliner entries. - /// - [Fact] - public void CrossModuleGenericMultiInliner() - { - var crossModuleGenericLib = new CompiledAssembly - { - AssemblyName = "CrossModuleGenericLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs"], - }; - var consumer = new CompiledAssembly - { - AssemblyName = "MultiInlinerConsumer", - SourceResourceNames = ["CrossModuleInlining/MultiInlinerConsumer.cs"], - References = [crossModuleGenericLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(CrossModuleGenericMultiInliner), - [ - new(consumer.AssemblyName, - [ - new CrossgenAssembly(crossModuleGenericLib) - { - Kind = Crossgen2InputKind.Reference, - Options = [Crossgen2AssemblyOption.CrossModuleOptimization], - }, - new CrossgenAssembly(consumer), - ]) - { - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - R2RAssert.HasManifestRef(reader, "CrossModuleGenericLib"); - R2RAssert.HasCrossModuleInliningInfo(reader); - - // Verify that GetValue has cross-module inliners from both GenericWrapperA and GenericWrapperB. - // This exercises the cross-module inliner parsing path where indices - // must be read as absolute values, not delta-accumulated, and validates - // that the resolved method names match the expected inliners. - R2RAssert.HasCrossModuleInliners(reader, "GetValue", "GenericWrapperA", "GenericWrapperB"); - } - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs deleted file mode 100644 index cc1853b48a5bb7..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Test: Cross-module async method inlining -// Validates that cross-module async compilation produces manifest refs -// and [ASYNC] variants for methods calling into a dependency library. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncCrossModule -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCrossModuleAsync() - { - return await AsyncDepLib.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCrossModuleStringAsync() - { - return await AsyncDepLib.GetStringAsync(); - } - - // Call a non-async sync method from async lib - [MethodImpl(MethodImplOptions.NoInlining)] - public static int CallCrossModuleSync() - { - return AsyncDepLib.GetValueSync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs deleted file mode 100644 index b4e2f87ecda45d..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Test: Non-composite runtime-async cross-module inlining with continuation layouts. -// The dependency methods capture GC refs across await points. -// Validates manifest refs and [ASYNC] variants for cross-module async calls. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncCrossModuleContinuation -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCrossModuleCaptureRef() - { - return await AsyncDepLibContinuation.CaptureRefAcrossAwait(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCrossModuleCaptureArray() - { - return await AsyncDepLibContinuation.CaptureArrayAcrossAwait(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs deleted file mode 100644 index 6170e4ac67360a..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Test: Async virtual method devirtualization in R2R -// Validates that async methods on sealed/interface types -// produce [ASYNC] variant entries in the R2R image. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public interface IAsyncService -{ - Task GetValueAsync(); -} - -public class OpenImpl : IAsyncService -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public virtual async Task GetValueAsync() - { - await Task.Yield(); - return 10; - } -} - -public sealed class SealedImpl : IAsyncService -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public async Task GetValueAsync() - { - await Task.Yield(); - return 20; - } -} - -public static class AsyncDevirtualize -{ - // Sealed type known at compile time — should devirtualize - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallOnSealed(SealedImpl obj) - { - return await obj.GetValueAsync(); - } - - // newobj gives exact type info — should devirtualize through resolveVirtualMethod - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallOnNewOpen() - { - IAsyncService svc = new OpenImpl(); - return await svc.GetValueAsync(); - } - - // Generic constrained dispatch - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallGenericConstrained(T obj) where T : IAsyncService - { - return await obj.GetValueAsync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs deleted file mode 100644 index 28ddb07949521c..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Test: Async method without yields (no suspension point) -// When a runtime-async method never actually awaits, crossgen2 may -// omit the resumption stub. This tests that edge case. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncNoYield -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task AsyncButNoAwait() - { - return 42; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task AsyncWithConditionalAwait(bool doAwait) - { - if (doAwait) - await Task.Yield(); - return 1; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs deleted file mode 100644 index e92cd6f1ae2de4..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Test: Non-composite runtime-async transitive cross-module inlining. -// Chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. -// Validates transitive manifest refs and async cross-module inlining. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncTransitiveMain -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallTransitiveValueAsync() - { - return await AsyncTransitiveLib.GetExternalValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallTransitiveLabelAsync() - { - return await AsyncTransitiveLib.GetExternalLabelAsync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs deleted file mode 100644 index 29a8fc879ec463..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Test: Async method that captures GC refs across await -// This forces the compiler to emit a ContinuationLayout fixup. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncWithContinuation -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CaptureObjectAcrossAwait() - { - object o = new object(); - string s = "hello"; - await Task.Yield(); - return s + o.GetHashCode(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CaptureMultipleRefsAcrossAwait() - { - int[] arr = new int[] { 1, 2, 3 }; - string text = "world"; - await Task.Yield(); - return arr[0] + text.Length; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs deleted file mode 100644 index 94a314a1b101fb..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Test: Basic async method emission in R2R -// Validates that runtime-async methods produce [ASYNC] variant entries -// in the R2R image. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class BasicAsyncEmission -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task SimpleAsyncMethod() - { - await Task.Yield(); - return 42; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task AsyncVoidReturn() - { - await Task.Yield(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async ValueTask ValueTaskMethod() - { - await Task.Yield(); - return "hello"; - } - - // Non-async method that returns Task (no await) — should NOT get async variant - [MethodImpl(MethodImplOptions.NoInlining)] - public static Task SyncTaskReturning() - { - return Task.FromResult(1); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs deleted file mode 100644 index 62a06bc816f0c2..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Test: Composite mode async devirtualization across module boundaries. -// Interface defined in AsyncInterfaceLib, call sites here. -// In composite mode, crossgen2 should devirtualize sealed type dispatch. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class CompositeAsyncDevirtMain -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallOnSealed(SealedAsyncService svc) - { - return await svc.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallOnNewOpen() - { - IAsyncCompositeService svc = new OpenAsyncService(); - return await svc.GetValueAsync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs deleted file mode 100644 index bb10453f70b05e..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Dependency library for async cross-module tests. -// Contains runtime-async methods that should be inlineable. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncDepLib -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetValueAsync() - { - await Task.Yield(); - return 42; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetStringAsync() - { - await Task.Yield(); - return "async_hello"; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetValueSync() - { - return 99; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs deleted file mode 100644 index b4124b80008fe1..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Dependency library for async cross-module continuation tests. -// Contains runtime-async methods that capture GC refs across await points, -// forcing ContinuationLayout fixup emission when cross-module inlined. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncDepLibContinuation -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task CaptureRefAcrossAwait() - { - object o = new object(); - string s = "cross_module"; - await Task.Yield(); - return s + o.GetHashCode(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task CaptureArrayAcrossAwait() - { - int[] arr = new int[] { 10, 20, 30 }; - string label = "sum"; - await Task.Yield(); - return arr[0] + arr[1] + label.Length; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs deleted file mode 100644 index f45db23f004570..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs +++ /dev/null @@ -1,13 +0,0 @@ -// External types for async transitive cross-module tests. -// Similar to ExternalLib but with async-friendly types. -using System; - -public static class AsyncExternalLib -{ - public static int ExternalValue => 77; - - public class AsyncExternalType - { - public string Label { get; set; } = "external"; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs deleted file mode 100644 index 528162b5d10c10..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Dependency library: defines an async interface and sealed implementation -// for cross-module async devirtualization tests in composite mode. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public interface IAsyncCompositeService -{ - Task GetValueAsync(); -} - -public sealed class SealedAsyncService : IAsyncCompositeService -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public async Task GetValueAsync() - { - await Task.Yield(); - return 42; - } -} - -public class OpenAsyncService : IAsyncCompositeService -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public virtual async Task GetValueAsync() - { - await Task.Yield(); - return 10; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs deleted file mode 100644 index 717040a417c362..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Middle library in async transitive chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. -// Contains runtime-async methods that reference types from AsyncExternalLib. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncTransitiveLib -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetExternalValueAsync() - { - await Task.Yield(); - return AsyncExternalLib.ExternalValue; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetExternalLabelAsync() - { - var ext = new AsyncExternalLib.AsyncExternalType(); - await Task.Yield(); - return ext.Label; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs deleted file mode 100644 index e481c3bcefd748..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Runtime.CompilerServices; - -[AttributeUsage(AttributeTargets.Method)] -internal class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribute -{ - public bool RuntimeAsync { get; } = runtimeAsync; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs deleted file mode 100644 index 3f2765f05b834d..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using Xunit.Abstractions; - -namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; - -/// -/// Known crossgen2 option kinds. -/// -internal enum Crossgen2AssemblyOption -{ - /// Enables cross-module inlining for a named assembly (--opt-cross-module AssemblyName). - CrossModuleOptimization, -} - -internal enum Crossgen2InputKind -{ - InputAssembly, - Reference, - InputBubbleReference, - UnrootedInputFile, -} - -internal enum Crossgen2Option -{ - Composite, - InputBubble, - ObjectFormat, - HotColdSplitting, - Optimize, - TargetArch, - TargetOS, -} - -internal static class Crossgen2OptionsExtensions -{ - public static string ToArg(this Crossgen2AssemblyOption kind) => kind switch - { - Crossgen2AssemblyOption.CrossModuleOptimization => $"--opt-cross-module", - _ => throw new ArgumentOutOfRangeException(nameof(kind)), - }; - - public static string ToArg(this Crossgen2InputKind kind) => kind switch - { - Crossgen2InputKind.InputAssembly => "", // positional argument - Crossgen2InputKind.Reference => $"--reference", - Crossgen2InputKind.InputBubbleReference => $"--inputbubbleref", - Crossgen2InputKind.UnrootedInputFile => $"--unrooted-input-file-paths", - _ => throw new ArgumentOutOfRangeException(nameof(kind)), - }; - - public static string ToArg(this Crossgen2Option kind) => kind switch - { - Crossgen2Option.Composite => $"--composite", - Crossgen2Option.InputBubble => $"--input-bubble", - Crossgen2Option.ObjectFormat => $"--object-format", - Crossgen2Option.HotColdSplitting => $"--hot-cold-splitting", - Crossgen2Option.Optimize => $"--optimize", - Crossgen2Option.TargetArch => $"--target-arch", - Crossgen2Option.TargetOS => $"--target-os", - _ => throw new ArgumentOutOfRangeException(nameof(kind)), - }; -} - -/// -/// Result of a crossgen2 compilation step. -/// -internal sealed record R2RCompilationResult( - int ExitCode, - string StandardOutput, - string StandardError) -{ - public bool Success => ExitCode == 0; -} - -/// -/// Invokes crossgen2 out-of-process to produce R2R images. -/// -internal sealed class R2RDriver -{ - private static readonly TimeSpan ProcessTimeout = TimeSpan.FromMinutes(2); - private readonly ITestOutputHelper _output; - - public R2RDriver(ITestOutputHelper output) - { - _output = output; - - if (!File.Exists(TestPaths.Crossgen2Exe)) - throw new FileNotFoundException($"crossgen2 executable not found at {TestPaths.Crossgen2Exe}"); - } - - /// - /// Runs crossgen2 with the given arguments. - /// - public R2RCompilationResult Compile(List crossgen2Args) - { - var psi = new ProcessStartInfo(TestPaths.Crossgen2Exe, crossgen2Args) - { - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - - string[] envVarsToStrip = { "DOTNET_GCName", "DOTNET_GCStress", "DOTNET_HeapVerify", "DOTNET_ReadyToRun" }; - foreach (string envVar in envVarsToStrip) - { - psi.Environment[envVar] = null; - } - - using var process = Process.Start(psi)!; - - // Read stdout and stderr concurrently to avoid pipe buffer deadlock. - // If crossgen2 fills one pipe while we block reading the other, both processes hang. - var stdoutTask = process.StandardOutput.ReadToEndAsync(); - var stderrTask = process.StandardError.ReadToEndAsync(); - - if (!process.WaitForExit(ProcessTimeout)) - { - try { process.Kill(entireProcessTree: true); } - catch { /* best effort */ } - throw new TimeoutException($"crossgen2 timed out after {ProcessTimeout.TotalMinutes} minutes"); - } - - string stdout = stdoutTask.GetAwaiter().GetResult(); - string stderr = stderrTask.GetAwaiter().GetResult(); - - if (process.ExitCode != 0) - { - _output.WriteLine($" crossgen2 FAILED (exit code {process.ExitCode})"); - _output.WriteLine(stderr); - } - - return new R2RCompilationResult( - process.ExitCode, - stdout, - stderr); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs deleted file mode 100644 index 9e7d0ec85930c6..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ /dev/null @@ -1,434 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection.Metadata; -using System.Reflection.PortableExecutable; -using ILCompiler.Reflection.ReadyToRun; -using Internal.ReadyToRunConstants; -using Internal.Runtime; -using Xunit; - -namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; - -/// -/// Static assertion helpers for validating R2R images via . -/// Use these in callbacks. -/// -internal static class R2RAssert -{ - /// - /// Returns all methods (assembly methods + instance methods) from the reader. - /// - public static List GetAllMethods(ReadyToRunReader reader) - { - var methods = new List(); - foreach (var assembly in reader.ReadyToRunAssemblies) - methods.AddRange(assembly.Methods); - foreach (var instanceMethod in reader.InstanceMethods) - methods.Add(instanceMethod.Method); - - return methods; - } - - /// - /// Asserts the R2R image contains a manifest or MSIL assembly reference with the given name. - /// - public static void HasManifestRef(ReadyToRunReader reader, string assemblyName) - { - var allRefs = new HashSet(StringComparer.OrdinalIgnoreCase); - - var globalMetadata = reader.GetGlobalMetadata(); - if (globalMetadata is not null) - { - var mdReader = globalMetadata.MetadataReader; - foreach (var handle in mdReader.AssemblyReferences) - { - var assemblyRef = mdReader.GetAssemblyReference(handle); - allRefs.Add(mdReader.GetString(assemblyRef.Name)); - } - } - - foreach (var kvp in reader.ManifestReferenceAssemblies) - allRefs.Add(kvp.Key); - - Assert.True(allRefs.Contains(assemblyName), - $"Expected assembly reference '{assemblyName}' not found. " + - $"Found: [{string.Join(", ", allRefs.OrderBy(s => s))}]"); - } - - /// - /// Asserts that the CrossModuleInlineInfo section records that - /// inlined , and that the inlinee is encoded as a cross-module - /// reference (ILBody import index, not a local MethodDef RID). - /// - public static void HasCrossModuleInlinedMethod(ReadyToRunReader reader, string inlinerMethodName, string inlineeMethodName) - { - var inliningInfo = GetCrossModuleInliningInfoSection(reader); - - var allPairs = new List(); - foreach (var (inlinerName, inlineeName, inlineeKind) in inliningInfo.GetInliningPairs()) - { - allPairs.Add($"{inlinerName} → {inlineeName} ({inlineeKind})"); - - if (inlinerName.Contains(inlinerMethodName, StringComparison.OrdinalIgnoreCase) && - inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) - { - Assert.True(inlineeKind == CrossModuleInliningInfoSection.InlineeReferenceKind.CrossModule, - $"Found inlining pair '{inlinerName} → {inlineeName}' but the inlinee is not encoded " + - $"as a cross-module reference ({inlineeKind}). Expected ILBody import encoding."); - return; - } - } - - Assert.Fail( - $"Expected cross-module inlining '{inlineeMethodName}' into '{inlinerMethodName}', but it was not found.\n" + - $"Recorded inlining pairs:\n {string.Join("\n ", allPairs)}"); - } - - /// - /// Asserts that the specified method has an entry in the import sections with a fixup for this method that has a ModuleOverride to the mutable module, - /// and method signature that resolves to the specified resolvedEntity - /// - public static void MethodHasMutableModuleFixupToken(ReadyToRunReader reader, string methodName, string resolvedEntity) - { - foreach(var section in reader.ImportSections) - { - foreach(var entry in section.Entries) - { - } - } - - Assert.Fail($"Method '{methodName}' with mutable module fixup token '{resolvedEntity}' not found."); - - } - - - /// - /// Asserts that the CrossModuleInlineInfo section has an entry for an inlinee matching - /// with cross-module inliners whose resolved names - /// contain each of the . This validates that - /// cross-module inliner indices (encoded as absolute ILBody import indices) resolve - /// to the correct method names. - /// - public static void HasCrossModuleInliners( - ReadyToRunReader reader, - string inlineeMethodName, - params string[] expectedInlinerNames) - { - var inliningInfo = GetCrossModuleInliningInfoSection(reader); - - foreach (var entry in inliningInfo.GetEntries()) - { - string inlineeName = inliningInfo.ResolveMethodName(entry.Inlinee); - if (!inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) - continue; - - var crossModuleInlinerNames = new List(); - foreach (var inliner in entry.Inliners) - { - if (inliner.IsCrossModule) - crossModuleInlinerNames.Add(inliningInfo.ResolveMethodName(inliner)); - } - - foreach (string expected in expectedInlinerNames) - { - Assert.True( - crossModuleInlinerNames.Any(n => n.Contains(expected, StringComparison.OrdinalIgnoreCase)), - $"Inlinee '{inlineeName}': expected a cross-module inliner matching '{expected}' " + - $"but found only:\n {string.Join("\n ", crossModuleInlinerNames)}"); - } - - return; - } - - var allEntries = new List(); - foreach (var (inlinerName, inlineeName, _) in inliningInfo.GetInliningPairs()) - allEntries.Add($"{inlinerName} → {inlineeName}"); - - Assert.Fail( - $"No CrossModuleInlineInfo entry found for inlinee matching '{inlineeMethodName}'.\n" + - $"All inlining pairs:\n {string.Join("\n ", allEntries)}"); - } - - /// - /// Asserts that any inlining info section (CrossModuleInlineInfo or InliningInfo2) records that - /// inlined . - /// Does not check whether the encoding is cross-module or local. - /// - public static void HasInlinedMethod(ReadyToRunReader reader, string inlinerMethodName, string inlineeMethodName) - { - var foundPairs = new List(); - - if (reader.ReadyToRunHeader.Sections.ContainsKey(ReadyToRunSectionType.CrossModuleInlineInfo)) - { - var inliningInfo = GetCrossModuleInliningInfoSection(reader); - foreach (var (inlinerName, inlineeName, _) in inliningInfo.GetInliningPairs()) - { - if (inlinerName.Contains(inlinerMethodName, StringComparison.OrdinalIgnoreCase) && - inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) - { - return; - } - foundPairs.Add($"[CXMI] {inlinerName} -> {inlineeName}"); - } - } - - foreach (var info2 in GetAllInliningInfo2Sections(reader)) - { - foreach (var (inlinerName, inlineeName) in info2.GetInliningPairs()) - { - if (inlinerName.Contains(inlinerMethodName, StringComparison.OrdinalIgnoreCase) && - inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) - { - return; - } - foundPairs.Add($"[II2] {inlinerName} -> {inlineeName}"); - } - } - - string pairList = foundPairs.Count > 0 - ? string.Join("\n ", foundPairs) - : "(none)"; - - Assert.Fail( - $"Expected inlining '{inlineeMethodName}' into '{inlinerMethodName}', but it was not found.\n" + - $"Found inlining pairs:\n {pairList}"); - } - - private static CrossModuleInliningInfoSection GetCrossModuleInliningInfoSection(ReadyToRunReader reader) - { - Assert.True( - reader.ReadyToRunHeader.Sections.TryGetValue( - ReadyToRunSectionType.CrossModuleInlineInfo, out ReadyToRunSection section), - "Expected CrossModuleInlineInfo section not found in R2R image."); - - int offset = reader.GetOffset(section.RelativeVirtualAddress); - int endOffset = offset + section.Size; - - return new CrossModuleInliningInfoSection(reader, offset, endOffset); - } - - private static IEnumerable GetAllInliningInfo2Sections(ReadyToRunReader reader) - { - // InliningInfo2 can appear in the global header - if (reader.ReadyToRunHeader.Sections.TryGetValue( - ReadyToRunSectionType.InliningInfo2, out ReadyToRunSection globalSection)) - { - int offset = reader.GetOffset(globalSection.RelativeVirtualAddress); - int endOffset = offset + globalSection.Size; - yield return new InliningInfoSection2(reader, offset, endOffset); - } - - // In composite images, InliningInfo2 is per-assembly - if (reader.ReadyToRunAssemblyHeaders is not null) - { - foreach (var asmHeader in reader.ReadyToRunAssemblyHeaders) - { - if (asmHeader.Sections.TryGetValue( - ReadyToRunSectionType.InliningInfo2, out ReadyToRunSection asmSection)) - { - int offset = reader.GetOffset(asmSection.RelativeVirtualAddress); - int endOffset = offset + asmSection.Size; - yield return new InliningInfoSection2(reader, offset, endOffset); - } - } - } - } - - /// - /// Asserts the R2R image contains a CrossModuleInlineInfo section with at least one entry. - /// - public static void HasCrossModuleInliningInfo(ReadyToRunReader reader) - { - Assert.True( - reader.ReadyToRunHeader.Sections.TryGetValue( - ReadyToRunSectionType.CrossModuleInlineInfo, out ReadyToRunSection section), - "Expected CrossModuleInlineInfo section not found in R2R image."); - - int offset = reader.GetOffset(section.RelativeVirtualAddress); - int endOffset = offset + section.Size; - var inliningInfo = new CrossModuleInliningInfoSection(reader, offset, endOffset); - string dump = inliningInfo.ToString(); - - Assert.True( - dump.Length > 0, - "CrossModuleInlineInfo section is present but contains no entries."); - } - - /// - /// Asserts the R2R image contains an [ASYNC] variant entry whose signature contains the given method name. - /// - public static void HasAsyncVariant(ReadyToRunReader reader, string methodName) - { - var asyncSigs = GetAllMethods(reader) - .Where(m => m.SignatureString.Contains("[ASYNC]", StringComparison.OrdinalIgnoreCase)) - .Select(m => m.SignatureString) - .ToList(); - - Assert.True( - asyncSigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)), - $"Expected [ASYNC] variant for '{methodName}' not found. " + - $"Async methods: [{string.Join(", ", asyncSigs)}]"); - } - - /// - /// Asserts the R2R image contains a [RESUME] stub entry whose signature contains the given method name. - /// - public static void HasResumptionStub(ReadyToRunReader reader, string methodName) - { - var resumeSigs = GetAllMethods(reader) - .Where(m => m.SignatureString.Contains("[RESUME]", StringComparison.OrdinalIgnoreCase)) - .Select(m => m.SignatureString) - .ToList(); - - Assert.True( - resumeSigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)), - $"Expected [RESUME] stub for '{methodName}' not found. " + - $"Resume methods: [{string.Join(", ", resumeSigs)}]"); - } - - /// - /// Asserts the R2R image contains at least one ContinuationLayout fixup. - /// - public static void HasContinuationLayout(ReadyToRunReader reader) - { - HasFixupKind(reader, ReadyToRunFixupKind.ContinuationLayout); - } - - /// - /// Asserts a method whose signature contains - /// has at least one ContinuationLayout fixup. - /// - public static void HasContinuationLayout(ReadyToRunReader reader, string methodName) - { - HasFixupKindOnMethod(reader, ReadyToRunFixupKind.ContinuationLayout, methodName); - } - - /// - /// Asserts the R2R image contains at least one ResumptionStubEntryPoint fixup. - /// - public static void HasResumptionStubFixup(ReadyToRunReader reader) - { - HasFixupKind(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint); - } - - /// - /// Asserts a method whose signature contains - /// has at least one ResumptionStubEntryPoint fixup. - /// - public static void HasResumptionStubFixup(ReadyToRunReader reader, string methodName) - { - HasFixupKindOnMethod(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint, methodName); - } - - /// - /// Asserts the R2R image contains at least one fixup of the given kind. - /// - public static void HasFixupKind(ReadyToRunReader reader, ReadyToRunFixupKind kind) - { - var presentKinds = new HashSet(); - foreach (var method in GetAllMethods(reader)) - { - if (method.Fixups is null) - continue; - foreach (var cell in method.Fixups) - { - if (cell.Signature is not null) - presentKinds.Add(cell.Signature.FixupKind); - } - } - - Assert.True(presentKinds.Contains(kind), - $"Expected fixup kind '{kind}' not found. " + - $"Present kinds: [{string.Join(", ", presentKinds)}]"); - } - - /// - /// Asserts a method whose signature contains - /// has at least one fixup of the given kind. - /// - public static void HasFixupKindOnMethod(ReadyToRunReader reader, ReadyToRunFixupKind kind, string methodName) - { - var methodsWithFixup = new List(); - foreach (var method in GetAllMethods(reader)) - { - if (method.Fixups is null) - continue; - - bool hasKind = false; - foreach (var cell in method.Fixups) - { - if (cell.Signature is not null && cell.Signature.FixupKind == kind) - { - hasKind = true; - break; - } - } - - if (hasKind) - { - methodsWithFixup.Add(method.SignatureString); - if (method.SignatureString.Contains(methodName, StringComparison.OrdinalIgnoreCase)) - return; - } - } - - Assert.Fail( - $"Expected fixup kind '{kind}' on method matching '{methodName}', but not found.\n" + - $"Methods with '{kind}' fixups: [{string.Join(", ", methodsWithFixup)}]"); - } -} - -/// -/// Simple assembly resolver that looks in the same directory as the input image. -/// -internal sealed class SimpleAssemblyResolver : IAssemblyResolver -{ - public IAssemblyMetadata? FindAssembly(MetadataReader metadataReader, AssemblyReferenceHandle assemblyReferenceHandle, string parentFile) - { - var assemblyRef = metadataReader.GetAssemblyReference(assemblyReferenceHandle); - string name = metadataReader.GetString(assemblyRef.Name); - - return FindAssembly(name, parentFile); - } - - public IAssemblyMetadata? FindAssembly(string simpleName, string parentFile) - { - string? dir = Path.GetDirectoryName(parentFile); - if (dir is null) - return null; - - string candidate = Path.Combine(dir, simpleName + ".dll"); - if (!File.Exists(candidate)) - candidate = Path.Combine(TestPaths.RuntimePackDir, simpleName + ".dll"); - - if (!File.Exists(candidate)) - return null; - - return new SimpleAssemblyMetadata(candidate); - } -} - -/// -/// Simple assembly metadata wrapper that loads the PE image into memory -/// to avoid holding file handles open (IAssemblyMetadata has no disposal contract, -/// and ReadyToRunReader caches these indefinitely). -/// -internal sealed class SimpleAssemblyMetadata : IAssemblyMetadata -{ - private readonly PEReader _peReader; - - public SimpleAssemblyMetadata(string path) - { - byte[] imageBytes = File.ReadAllBytes(path); - _peReader = new PEReader(new MemoryStream(imageBytes)); - } - - public PEReader ImageReader => _peReader; - - public MetadataReader MetadataReader => _peReader.GetMetadataReader(); -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs deleted file mode 100644 index b4801fc8f22583..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestCaseCompiler.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Reflection.Metadata; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Emit; - -namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; - -/// -/// Compiles C# source code into assemblies using Roslyn at test time. -/// -internal sealed class R2RTestCaseCompiler -{ - private readonly List _frameworkReferences; - - public R2RTestCaseCompiler() - { - _frameworkReferences = new List(); - - // Add reference assemblies from the ref pack (needed for Roslyn compilation) - string refPackDir = TestPaths.RefPackDir; - if (Directory.Exists(refPackDir)) - { - foreach (string refPath in Directory.EnumerateFiles(refPackDir, "*.dll")) - { - _frameworkReferences.Add(MetadataReference.CreateFromFile(refPath)); - } - } - else - { - // Fallback to runtime pack implementation assemblies - foreach (string refPath in TestPaths.GetFrameworkReferencePaths()) - { - _frameworkReferences.Add(MetadataReference.CreateFromFile(refPath)); - } - } - } - - /// - /// Compiles a single assembly from source files. - /// - /// Name of the output assembly (without .dll extension). - /// C# source code strings. - /// Paths to additional assembly references. - /// Library or ConsoleApplication. - /// Additional preprocessor defines. - /// Roslyn feature flags (e.g. "runtime-async=on"). - /// Path to the compiled assembly. - public string CompileAssembly( - string assemblyName, - IEnumerable sources, - string outputPath, - OutputKind outputKind, - IEnumerable additionalReferences, - IEnumerable? additionalDefines = null, - IEnumerable>? features = null) - { - var parseOptions = new CSharpParseOptions( - LanguageVersion.Latest, - preprocessorSymbols: additionalDefines); - - if (features is not null) - parseOptions = parseOptions.WithFeatures(features); - - var syntaxTrees = sources.Select(src => - CSharpSyntaxTree.ParseText(src, parseOptions)); - - var references = new List(_frameworkReferences); - if (additionalReferences is not null) - { - foreach (string refPath in additionalReferences) - { - references.Add(MetadataReference.CreateFromFile(refPath)); - } - } - - var compilation = CSharpCompilation.Create( - assemblyName, - syntaxTrees, - references, - new CSharpCompilationOptions(outputKind) - .WithOptimizationLevel(OptimizationLevel.Release) - .WithAllowUnsafe(true) - .WithNullableContextOptions(NullableContextOptions.Enable)); - - EmitResult result = compilation.Emit(outputPath); - - if (!result.Success) - { - var errors = result.Diagnostics - .Where(d => d.Severity == DiagnosticSeverity.Error) - .Select(d => d.ToString()); - throw new InvalidOperationException( - $"Compilation of '{assemblyName}' failed:\n{string.Join("\n", errors)}"); - } - - return outputPath; - } - - /// - /// Reads an embedded resource from the test assembly. - /// - public static string ReadEmbeddedSource(string resourceName) - { - var assembly = Assembly.GetExecutingAssembly(); - using Stream? stream = assembly.GetManifestResourceStream(resourceName); - if (stream is null) - { - // Try with different path separator - string altName = resourceName.Replace('/', '\\'); - using Stream? altStream = assembly.GetManifestResourceStream(altName); - if (altStream is null) - throw new FileNotFoundException($"Embedded resource not found: '{resourceName}'. Available: {string.Join(", ", assembly.GetManifestResourceNames())}"); - - using var altReader = new StreamReader(altStream); - return altReader.ReadToEnd(); - } - - using var reader = new StreamReader(stream); - return reader.ReadToEnd(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs deleted file mode 100644 index 4b829aef41d48c..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +++ /dev/null @@ -1,410 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using ILCompiler.Reflection.ReadyToRun; -using Microsoft.CodeAnalysis; -using Xunit; -using Xunit.Abstractions; - -namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; - -/// -/// Describes an assembly compiled by Roslyn as part of a test case. -/// -internal sealed class CompiledAssembly -{ - public required string AssemblyName { get; init; } - - /// - /// The name of the string resources that contain the source code for this assembly. - /// - public required string[] SourceResourceNames { get; init; } - - /// - /// Roslyn feature flags for this assembly (e.g. runtime-async=on). - /// - public List> Features { get; init; } = new(); - - /// - /// References to other assemblies that this assembly depends on. - /// - public List References { get; init; } = new(); - - public OutputKind OutputKind {get; init;} = OutputKind.DynamicallyLinkedLibrary; - - private string? _outputDir = null; - public string FilePath => _outputDir != null ? Path.Combine(_outputDir, "IL", AssemblyName + ".dll") - : throw new InvalidOperationException("Output directory not set"); - - public void SetOutputDir(string outputDir) - { - _outputDir = outputDir; - } -} - -/// -/// References a within a , -/// specifying its role and per-assembly options. -/// -internal sealed class CrossgenAssembly(CompiledAssembly ilAssembly) -{ - public CompiledAssembly ILAssembly => ilAssembly; - /// - /// How this assembly is passed to crossgen2. - /// Defaults to . - /// - public Crossgen2InputKind Kind { get; init; } = Crossgen2InputKind.InputAssembly; - /// - /// Per-assembly crossgen2 options (e.g. cross-module optimization targets). - /// - public List Options { get; init; } = new(); - - public void SetOutputDir(string outputDir) - { - ILAssembly.SetOutputDir(outputDir); - } -} - -/// -/// A single crossgen2 compilation step. -/// -internal sealed class CrossgenCompilation(string name, List assemblies) -{ - /// - /// Assemblies involved in this compilation. Each specifies its role - /// () and per-assembly options. - /// All other Roslyn-compiled assemblies are automatically available as refs. - /// - public List Assemblies => assemblies; - - /// - /// Image-level crossgen2 options (e.g. Composite, InputBubble, Optimize). - /// - public List Options { get; init; } = new(); - - /// - /// Optional validator for this compilation's R2R output image. - /// - public Action? Validate { get; init; } - - /// - /// When set, the R2R image is executed with corerun after validation. - /// The value is the name of the entry-point assembly (must have been - /// compiled as ). - /// Exit code 0 = pass; non-zero fails the test. - /// - public CrossgenAssembly? Execute { get; init; } = null; - - public string Name => name; - - public bool IsComposite => Options.Contains(Crossgen2Option.Composite); - - private string? _outputDir = null; - - /// - /// The output path for this compilation. In composite mode, uses a "-composite" suffix - /// to avoid colliding with component stubs that crossgen2 creates alongside the composite image. - /// - public string FilePath => _outputDir != null - ? Path.Combine(_outputDir, "CG2", Name + (IsComposite ? "-composite" : "") + ".dll") - : throw new InvalidOperationException("Output directory not set"); - - public void SetOutputDir(string outputDir) - { - _outputDir = outputDir; - foreach (var assembly in assemblies) - { - assembly.SetOutputDir(outputDir); - } - } -} - -/// -/// Describes a test case: assemblies compiled with Roslyn, then crossgen2'd in one or more -/// compilation steps, each optionally validated. -/// -internal sealed class R2RTestCase(string name, List compilations) -{ - public string Name => name; - - /// - /// One or more crossgen2 compilation steps, executed after Roslyn compilation. - /// Each step can independently produce and validate an R2R image. - /// - public List Compilations => compilations; - - /// - /// Returns a list of assemblies to compile with Roslyn, in such an order that dependencies are compiled before the assemblies that depend on them. - /// - public IEnumerable GetAssemblies() - { - // Should be a small number of assemblies, so a simple list is fine as an insertion-ordered set - List seen = new(); - foreach (var cg2Compilation in compilations) - { - foreach(var assembly in cg2Compilation.Assemblies) - { - GetDependencies(assembly.ILAssembly, seen); - } - } - return seen; - - IEnumerable GetDependencies(CompiledAssembly assembly, List seen) - { - foreach(var reference in assembly.References) - { - GetDependencies(reference, seen); - } - if (!seen.Contains(assembly)) - { - seen.Add(assembly); - } - return seen; - } - } - - public void SetOutputDir(string outputDir) - { - Compilations.ForEach(c => c.SetOutputDir(outputDir)); - } -} - -/// -/// Orchestrates the full R2R test pipeline: Roslyn compile → crossgen2 → validate. -/// -internal sealed class R2RTestRunner -{ - private readonly ITestOutputHelper _output; - - public R2RTestRunner(ITestOutputHelper output) - { - _output = output; - } - - /// - /// Runs a test case end-to-end. - /// - public void Run(R2RTestCase testCase) - { - var assembliesToCompile = testCase.GetAssemblies(); - Assert.True(assembliesToCompile.Count() > 0, "Test case must have at least one assembly."); - Assert.True(testCase.Compilations.Count > 0, "Test case must have at least one compilation."); - - string baseOutputDir = Path.Combine(AppContext.BaseDirectory, "R2RTestCases", testCase.Name, Guid.NewGuid().ToString("N")[..8]); - testCase.SetOutputDir(baseOutputDir); - - _output.WriteLine($"Test '{testCase.Name}': baseOutputDir = {baseOutputDir}"); - - try - { - // Step 1: Compile all assemblies with Roslyn in order - var assemblyPaths = CompileAllAssemblies(assembliesToCompile); - - // Step 2: Run each crossgen2 compilation and validate - var driver = new R2RDriver(_output); - var refPaths = BuildReferencePaths(); - - foreach(var compilation in testCase.Compilations) - { - string outputPath = RunCrossgenCompilation( - testCase.Name, compilation, driver, compilation.FilePath, refPaths, assemblyPaths); - - if (compilation.Validate is not null) - { - Assert.True(File.Exists(outputPath), $"R2R image not found: {outputPath}"); - _output.WriteLine($" Validating R2R image: {outputPath}"); - var reader = new ReadyToRunReader(new SimpleAssemblyResolver(), outputPath); - compilation.Validate(reader); - } - if (compilation.Execute is not null) - { - string exeAssemblyName = compilation.Execute.ILAssembly.AssemblyName; - string cg2Dir = Path.GetDirectoryName(outputPath)!; - - // Copy dependency IL assemblies next to the R2R exe so corerun can probe them. - foreach (var asm in compilation.Assemblies) - { - if (asm == compilation.Execute) - continue; - string src = asm.ILAssembly.FilePath; - string dest = Path.Combine(cg2Dir, Path.GetFileName(src)); - if (File.Exists(src) && !File.Exists(dest)) - File.Copy(src, dest); - } - - string exePath = Path.Combine(cg2Dir, exeAssemblyName + ".dll"); - Assert.True(File.Exists(exePath), $"Entry-point assembly not found: {exePath}"); - - // Use corerun from the testhost shared framework dir with CORE_ROOT - // pointing there. It has corerun, libcoreclr, managed framework DLLs, - // and native shims (libSystem.Native, etc.) — all in one directory. - _output.WriteLine($" Executing R2R image with corerun: {exePath}"); - var psi = new ProcessStartInfo(TestPaths.CorerunPath, [exePath]) - { - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - - using var process = Process.Start(psi)!; - var stdoutTask = process.StandardOutput.ReadToEndAsync(); - var stderrTask = process.StandardError.ReadToEndAsync(); - - if (!process.WaitForExit(TimeSpan.FromSeconds(30))) - { - try { process.Kill(entireProcessTree: true); } - catch { /* best effort */ } - Assert.Fail($"Execution of '{exeAssemblyName}' timed out after 30 seconds"); - } - - string stdout = stdoutTask.GetAwaiter().GetResult(); - string stderr = stderrTask.GetAwaiter().GetResult(); - - if (!string.IsNullOrWhiteSpace(stdout)) - _output.WriteLine($" stdout: {stdout}"); - if (!string.IsNullOrWhiteSpace(stderr)) - _output.WriteLine($" stderr: {stderr}"); - - Assert.True(process.ExitCode == 0, - $"Execution of '{exeAssemblyName}' failed (exit code {process.ExitCode}):\n{stderr}"); - } - - } - } - finally - { - if (Environment.GetEnvironmentVariable("KEEP_R2R_TESTS") is null) - { - try { Directory.Delete(baseOutputDir, true); } - catch { /* best effort */ } - } - } - } - - private Dictionary CompileAllAssemblies( - IEnumerable assemblies) - { - var compiler = new R2RTestCaseCompiler(); - var paths = new Dictionary(); - - foreach (var asm in assemblies) - { - var sources = asm.SourceResourceNames - .Select(R2RTestCaseCompiler.ReadEmbeddedSource) - .ToList(); - - EnsureDirectoryExists(Path.GetDirectoryName(asm.FilePath)); - - string ilPath = compiler.CompileAssembly( - asm.AssemblyName, - sources, - asm.FilePath, - asm.OutputKind, - additionalReferences: asm.References.Select(r => r.FilePath).ToList(), - features: asm.Features.Count > 0 ? asm.Features : null); - paths[asm.AssemblyName] = ilPath; - _output.WriteLine($" Roslyn compiled '{asm.AssemblyName}' -> {ilPath}"); - } - - return paths; - } - - private static void EnsureDirectoryExists(string? v) - { - if (v is not null && !Directory.Exists(v)) - { - Directory.CreateDirectory(v); - } - } - - private static string RunCrossgenCompilation( - string testName, - CrossgenCompilation compilation, - R2RDriver driver, - string outputFile, - List refPaths, - Dictionary assemblyPaths) - { - var args = new List(); - - var inputFiles = new List(); - // Per-assembly inputs and options - foreach (var asm in compilation.Assemblies) - { - var ilAssemblyName = asm.ILAssembly.AssemblyName; - Assert.True(assemblyPaths.ContainsKey(ilAssemblyName), - $"Assembly '{ilAssemblyName}' not found in compiled assemblies."); - - string ilPath = asm.ILAssembly.FilePath; - - if (asm.Kind == Crossgen2InputKind.InputAssembly) - { - inputFiles.Add(ilPath); - } - else - { - args.Add(asm.Kind.ToArg()); - args.Add(ilPath); - } - - foreach (var option in asm.Options) - { - args.Add(option.ToArg()); - args.Add(ilAssemblyName); - } - } - - // Image-level options - foreach (var option in compilation.Options) - args.Add(option.ToArg()); - - // Global refs (runtime pack + System.Private.CoreLib) - AddRefArgs(args, refPaths); - - EnsureDirectoryExists(Path.GetDirectoryName(outputFile)); - - inputFiles.AddRange(args); - args = inputFiles; - args.Add($"--out"); - args.Add($"{outputFile}"); - var result = driver.Compile(args); - Assert.True(result.Success, - $"crossgen2 failed for '{testName}':\n{result.StandardError}\n{result.StandardOutput}"); - - return outputFile; - } - - private static void AddRefArgs(List args, List refPaths) - { - foreach (string refPath in refPaths) - { - args.Add("-r"); - args.Add(refPath); - } - } - - private static List BuildReferencePaths() - { - var paths = new List(); - - paths.Add(Path.Combine(TestPaths.RuntimePackDir, "*.dll")); - - string runtimePackDir = TestPaths.RuntimePackDir; - string nativeDir = Path.GetFullPath(Path.Combine(runtimePackDir, "..", "..", "native")); - if (Directory.Exists(nativeDir)) - { - string spcl = Path.Combine(nativeDir, "System.Private.CoreLib.dll"); - if (File.Exists(spcl)) - paths.Add(spcl); - } - - return paths; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs deleted file mode 100644 index bd5f845111d280..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/TestPaths.cs +++ /dev/null @@ -1,192 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; - -namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner; - -/// -/// Provides paths to build artifacts needed by the test infrastructure. -/// All paths come from RuntimeHostConfigurationOption items in the csproj. -/// -internal static class TestPaths -{ - private static string GetRequiredConfig(string key) - { - return AppContext.GetData(key) as string - ?? throw new InvalidOperationException($"Missing RuntimeHostConfigurationOption '{key}'. Was the project built with the correct properties?"); - } - - /// - /// Path to the crossgen2 output directory (contains crossgen2.dll and clrjit). - /// e.g. artifacts/bin/coreclr/linux.x64.Checked/crossgen2/ - /// Falls back to Checked or Release if Debug path doesn't exist. - /// - public static string Crossgen2Dir - { - get - { - string dir = GetRequiredConfig("R2RTest.Crossgen2Dir"); - if (!File.Exists(Path.Combine(dir, "crossgen2.dll"))) - { - // Try Checked and Release fallbacks since crossgen2 may be built in a different config - foreach (string fallbackConfig in new[] { "Checked", "Release", "Debug" }) - { - string fallback = Regex.Replace( - dir, @"\.(Debug|Release|Checked)[/\\]", $".{fallbackConfig}/"); - if (File.Exists(Path.Combine(fallback, "crossgen2.dll"))) - return fallback; - } - } - - return dir; - } - } - - /// - /// Path to the native crossgen2 executable (apphost). - /// - public static string Crossgen2Exe - { - get - { - string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "crossgen2.exe" : "crossgen2"; - return Path.Combine(Crossgen2Dir, exe); - } - } - - /// - /// Path to the runtime pack managed assemblies directory. - /// e.g. artifacts/bin/microsoft.netcore.app.runtime.linux-x64/Release/runtimes/linux-x64/lib/net11.0/ - /// Falls back to Release if Debug path doesn't exist (libs are typically built Release). - /// - public static string RuntimePackDir - { - get - { - string dir = GetRequiredConfig("R2RTest.RuntimePackDir"); - if (!Directory.Exists(dir) && dir.Contains("Debug")) - { - string releaseFallback = dir.Replace("Debug", "Release"); - if (Directory.Exists(releaseFallback)) - return releaseFallback; - } - - return dir; - } - } - - /// - /// Path to the CoreCLR artifacts directory (contains native bits like corerun). - /// e.g. artifacts/bin/coreclr/linux.x64.Checked/ - /// Falls back to Checked or Release if Debug path doesn't exist. - /// - public static string CoreCLRArtifactsDir - { - get - { - string dir = GetRequiredConfig("R2RTest.CoreCLRArtifactsDir"); - if (!Directory.Exists(dir)) - { - foreach (string fallbackConfig in new[] { "Checked", "Release", "Debug" }) - { - string fallback = Regex.Replace( - dir, @"\.(Debug|Release|Checked)(/|\\|$)", $".{fallbackConfig}$2"); - if (Directory.Exists(fallback)) - return fallback; - } - } - - return dir; - } - } - - public static string TargetArchitecture => GetRequiredConfig("R2RTest.TargetArchitecture"); - public static string TargetOS => GetRequiredConfig("R2RTest.TargetOS"); - public static string Configuration => GetRequiredConfig("R2RTest.Configuration"); - - /// - /// Path to the reference assembly pack (for Roslyn compilation). - /// e.g. artifacts/bin/microsoft.netcore.app.ref/ref/net11.0/ - /// - public static string RefPackDir - { - get - { - string dir = GetRequiredConfig("R2RTest.RefPackDir"); - if (!Directory.Exists(dir)) - { - // Try the artifacts/bin/ref/net* fallback - string artifactsBin = Path.GetFullPath(Path.Combine(CoreCLRArtifactsDir, "..", "..")); - string refDir = Path.Combine(artifactsBin, "ref"); - if (Directory.Exists(refDir)) - { - foreach (string subDir in Directory.GetDirectories(refDir, "net*")) - { - if (File.Exists(Path.Combine(subDir, "System.Runtime.dll"))) - return subDir; - } - } - } - - return dir; - } - } - - /// - /// Returns the target triple string for crossgen2 (e.g. "linux-x64"). - /// - public static string TargetTriple => $"{TargetOS.ToLowerInvariant()}-{TargetArchitecture.ToLowerInvariant()}"; - - /// - /// Path to the testhost shared framework directory containing corerun, - /// libcoreclr, all managed framework DLLs, and native shims. - /// e.g. artifacts/bin/testhost/net11.0-linux-Release-x64/shared/Microsoft.NETCore.App/11.0.0/ - /// - public static string TestHostSharedFrameworkDir - { - get - { - string dir = GetRequiredConfig("R2RTest.TestHostSharedFrameworkDir"); - if (!Directory.Exists(dir)) - { - foreach (string fallbackConfig in new[] { "Release", "Debug", "Checked" }) - { - string fallback = Regex.Replace( - dir, @"-(Debug|Release|Checked)-", $"-{fallbackConfig}-"); - if (Directory.Exists(fallback)) - return fallback; - } - } - - return dir; - } - } - - /// - /// Path to the corerun executable inside the testhost shared framework. - /// - public static string CorerunPath - { - get - { - string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "corerun.exe" : "corerun"; - return Path.Combine(TestHostSharedFrameworkDir, exe); - } - } - - /// - /// Returns all framework reference assembly paths (*.dll in the runtime pack). - /// - public static IEnumerable GetFrameworkReferencePaths() - { - if (!Directory.Exists(RuntimePackDir)) - throw new DirectoryNotFoundException($"Runtime pack directory not found: {RuntimePackDir}"); - - return Directory.EnumerateFiles(RuntimePackDir, "*.dll"); - } -} diff --git a/src/coreclr/tools/aot/crossgen2.slnx b/src/coreclr/tools/aot/crossgen2.slnx index b2cd7b76b2bf64..b79a67596c466f 100644 --- a/src/coreclr/tools/aot/crossgen2.slnx +++ b/src/coreclr/tools/aot/crossgen2.slnx @@ -46,9 +46,6 @@ - - - diff --git a/src/coreclr/tools/r2rdump/TextDumper.cs b/src/coreclr/tools/r2rdump/TextDumper.cs index 370f80c80b917c..5fe9277c830a0d 100644 --- a/src/coreclr/tools/r2rdump/TextDumper.cs +++ b/src/coreclr/tools/r2rdump/TextDumper.cs @@ -87,6 +87,8 @@ public override void DumpHeader(bool dumpSections) int assemblyIndex = 0; foreach (string assemblyName in _r2r.ManifestReferenceAssemblies.OrderBy(kvp => kvp.Value).Select(kvp => kvp.Key)) { + if (assemblyIndex >= _r2r.ReadyToRunAssemblyHeaders.Count) + break; Guid mvid = _r2r.GetAssemblyMvid(assemblyIndex); WriteDivider($@"Component Assembly [{assemblyIndex}]: {assemblyName} - MVID {mvid:b}"); ReadyToRunCoreHeader assemblyHeader = _r2r.ReadyToRunAssemblyHeaders[assemblyIndex]; @@ -504,12 +506,6 @@ public override void DumpSectionContents(ReadyToRunSection section) InliningInfoSection2 inliningInfoSection2 = new InliningInfoSection2(_r2r, ii2Offset, ii2EndOffset); _writer.WriteLine(inliningInfoSection2.ToString()); break; - case ReadyToRunSectionType.CrossModuleInlineInfo: - int cmiOffset = _r2r.GetOffset(section.RelativeVirtualAddress); - int cmiEndOffset = cmiOffset + section.Size; - CrossModuleInliningInfoSection crossModuleInliningInfo = new CrossModuleInliningInfoSection(_r2r, cmiOffset, cmiEndOffset); - _writer.WriteLine(crossModuleInliningInfo.ToString()); - break; case ReadyToRunSectionType.OwnerCompositeExecutable: int oceOffset = _r2r.GetOffset(section.RelativeVirtualAddress); if (_r2r.Image[oceOffset + section.Size - 1] != 0) From 1bab3b6d4c139442544f721f3dfeb862d237e83c Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:47:56 -0700 Subject: [PATCH 37/74] Remove .github changes --- .github/actions/select-copilot-pat/README.md | 7 + .github/agents/agentic-workflows.agent.md | 102 +- .github/aw/actions-lock.json | 32 +- .github/dependabot.yml | 2 + .../Build-IssueComment.ps1 | 94 ++ .../breaking-change-doc/Get-VersionInfo.ps1 | 196 +++ .github/skills/breaking-change-doc/SKILL.md | 307 +++++ .github/skills/update-os-coverage/SKILL.md | 306 +++++ .../workflows/breaking-change-doc.lock.yml | 1194 +++++++++++++++++ .github/workflows/breaking-change-doc.md | 142 ++ .github/workflows/code-review.lock.yml | 10 +- .github/workflows/copilot-echo.lock.yml | 10 +- .github/workflows/labeler-cache-retention.yml | 2 +- .github/workflows/labeler-predict-issues.yml | 4 +- .github/workflows/labeler-predict-pulls.yml | 4 +- .github/workflows/labeler-promote.yml | 4 +- .github/workflows/labeler-train.yml | 12 +- 17 files changed, 2373 insertions(+), 55 deletions(-) create mode 100644 .github/skills/breaking-change-doc/Build-IssueComment.ps1 create mode 100644 .github/skills/breaking-change-doc/Get-VersionInfo.ps1 create mode 100644 .github/skills/breaking-change-doc/SKILL.md create mode 100644 .github/skills/update-os-coverage/SKILL.md create mode 100644 .github/workflows/breaking-change-doc.lock.yml create mode 100644 .github/workflows/breaking-change-doc.md diff --git a/.github/actions/select-copilot-pat/README.md b/.github/actions/select-copilot-pat/README.md index 12eeafe5b1f817..8b57f6377bcc84 100644 --- a/.github/actions/select-copilot-pat/README.md +++ b/.github/actions/select-copilot-pat/README.md @@ -10,6 +10,11 @@ To use Agentic Workflows in a dotnet org repository: 2. Copy this `select-copilot-pat` folder into the repository under `.github/actions/select-copilot-pat`, including both the `README.md` and `action.yml`. 3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below. +> **Optional:** If you plan to manage secrets or workflows from the command line (e.g., `gh aw secrets set`), [install the `gh aw` CLI extension][cli-setup]: +> ```sh +> gh extension install github/gh-aw +> ``` + ## PAT Management Team members provide PATs into the pools for the repository by adding them as repository secrets with secret names matching the pattern of `_<0-9>`, such as `COPILOT_PAT_0`. @@ -85,6 +90,7 @@ engine: ## References +- [Agentic Workflows CLI Extension][cli-setup] - [Agentic Authoring][configure-repo] - [Authentication][authentication] - [Agentic Workflow Imports][imports] @@ -96,6 +102,7 @@ engine: - [Case Function in Workflow Expressions][case-expression] - [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override] +[cli-setup]: https://github.github.com/gh-aw/setup/cli/ [configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository [authentication]: https://github.github.com/gh-aw/reference/auth/ [create-pat]: https://github.com/settings/personal-access-tokens/new?name=dotnet%20org%20agentic%20workflows&description=GitHub+Agentic+Workflows+-+Copilot+engine+authentication.++Used+for+dotnet+org+workflows.+MUST+be+configured+with+only+Copilot+Requests+permissions+and+user+account+as+resource+owner.+Weekly+expiration+and+required+renewal.&user_copilot_requests=read&expires_in=8 diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.agent.md index 7ed300e00cc160..b8e305fc4628df 100644 --- a/.github/agents/agentic-workflows.agent.md +++ b/.github/agents/agentic-workflows.agent.md @@ -30,7 +30,7 @@ Workflows may optionally include: - Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` - Workflow lock files: `.github/workflows/*.lock.yml` - Shared components: `.github/workflows/shared/*.md` -- Configuration: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/github-agentic-workflows.md +- Configuration: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/github-agentic-workflows.md ## Problems This Solves @@ -52,7 +52,7 @@ When you interact with this agent, it will: ### Create New Workflow **Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/create-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/create-agentic-workflow.md **Use cases**: - "Create a workflow that triages issues" @@ -62,7 +62,7 @@ When you interact with this agent, it will: ### Update Existing Workflow **Load when**: User wants to modify, improve, or refactor an existing workflow -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/update-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/update-agentic-workflow.md **Use cases**: - "Add web-fetch tool to the issue-classifier workflow" @@ -72,7 +72,7 @@ When you interact with this agent, it will: ### Debug Workflow **Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/debug-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/debug-agentic-workflow.md **Use cases**: - "Why is this workflow failing?" @@ -82,7 +82,7 @@ When you interact with this agent, it will: ### Upgrade Agentic Workflows **Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/upgrade-agentic-workflows.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/upgrade-agentic-workflows.md **Use cases**: - "Upgrade all workflows to the latest version" @@ -92,7 +92,7 @@ When you interact with this agent, it will: ### Create a Report-Generating Workflow **Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/report.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/report.md **Use cases**: - "Create a weekly CI health report" @@ -102,7 +102,7 @@ When you interact with this agent, it will: ### Create Shared Agentic Workflow **Load when**: User wants to create a reusable workflow component or wrap an MCP server -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/create-shared-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/create-shared-agentic-workflow.md **Use cases**: - "Create a shared component for Notion integration" @@ -112,7 +112,7 @@ When you interact with this agent, it will: ### Fix Dependabot PRs **Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`) -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/dependabot.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/dependabot.md **Use cases**: - "Fix the open Dependabot PRs for npm dependencies" @@ -122,7 +122,7 @@ When you interact with this agent, it will: ### Analyze Test Coverage **Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy. -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/test-coverage.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/test-coverage.md **Use cases**: - "Create a workflow that comments coverage on PRs" @@ -169,10 +169,90 @@ gh aw compile --validate ## Important Notes -- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/github-agentic-workflows.md for complete documentation +- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/github-agentic-workflows.md for complete documentation - Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud - Workflows must be compiled to `.lock.yml` files before running in GitHub Actions - **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF - Follow security best practices: minimal permissions, explicit network access, no template injection -- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/network.md for the full list of valid ecosystem identifiers and domain patterns. +- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See https://github.com/github/gh-aw/blob/v0.63.0/.github/aw/network.md for the full list of valid ecosystem identifiers and domain patterns. - **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself. + +## Repository-Specific Requirements: Copilot PAT Pool + +**⚠️ MANDATORY**: Every agentic workflow in this repository **must** include the `select-copilot-pat` integration. This selects a random Copilot PAT from a numbered pool of secrets to avoid rate-limiting from a single shared PAT. This is a stop-gap workaround until organization-level billing is offered for agentic workflows. + +These elements are **not supported through imports**, so they must be copied into the frontmatter of every agentic workflow. See `.github/actions/select-copilot-pat/README.md` for full documentation and `.github/workflows/copilot-echo.md` for a working example. + +When **creating** or **updating** an agentic workflow, always include the following three frontmatter blocks. These go between the `---` markers alongside the workflow's other frontmatter (permissions, safe-outputs, etc.). + +### Comment block (placed before the steps, inside the `on:` block) + +Always include this comment block before the steps to explain the PAT pool override: + +```yaml +# ############################################################### +# Override the COPILOT_GITHUB_TOKEN secret usage for the workflow +# with a randomly-selected token from a pool of secrets. +# +# As soon as organization-level billing is offered for Agentic +# Workflows, this stop-gap approach will be removed. +# +# See: /.github/actions/select-copilot-pat/README.md +# ############################################################### +``` + +### Block 1: Pre-activation steps (nested under `on:`) + +Add this under the `on:` key, at the same level as the trigger configuration, immediately after the comment block above: + +```yaml + # Add the pre-activation step of selecting a random PAT from the supplied secrets + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + name: Checkout the select-copilot-pat action folder + with: + persist-credentials: false + sparse-checkout: .github/actions/select-copilot-pat + sparse-checkout-cone-mode: true + fetch-depth: 1 + + - id: select-copilot-pat + name: Select Copilot token from pool + uses: ./.github/actions/select-copilot-pat + env: + SECRET_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_9: ${{ secrets.COPILOT_PAT_9 }} +``` + +### Block 2: Pre-activation job outputs (top-level `jobs:`) + +```yaml +jobs: + pre-activation: + outputs: + copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} +``` + +### Block 3: Engine configuration with PAT override (top-level `engine:`) + +```yaml +engine: + id: copilot + env: + # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow + # If none of the `COPILOT_PAT_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} +``` + +**Important notes about the engine block:** +- The `COPILOT_GITHUB_TOKEN` `case()` expression **must** remain on a single line — line breaks cause syntax errors in the compiled workflow. +- If no `COPILOT_PAT_#` secrets are configured, the expression falls back to the default `COPILOT_GITHUB_TOKEN` secret. +- Do **not** specify `engine: copilot` as a simple string — use the object form shown above so the `env:` override can be included. diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 1cd3695af0529a..4ff4a7ea77497e 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,29 +1,14 @@ { "entries": { - "actions/cache/restore@v5.0.4": { - "repo": "actions/cache/restore", - "version": "v5.0.4", - "sha": "668228422ae6a00e4ad889ee87cd7109ec5666a7" - }, - "actions/cache/save@v5.0.4": { - "repo": "actions/cache/save", - "version": "v5.0.4", - "sha": "668228422ae6a00e4ad889ee87cd7109ec5666a7" - }, - "actions/download-artifact@v8.0.1": { - "repo": "actions/download-artifact", - "version": "v8.0.1", - "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" - }, "actions/github-script@v8": { "repo": "actions/github-script", "version": "v8", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" }, - "actions/upload-artifact@v7": { + "actions/upload-artifact@v4": { "repo": "actions/upload-artifact", - "version": "v7", - "sha": "bbbca2ddaa5d8feaa63e36b76fdaad77386f024f" + "version": "v4", + "sha": "ea165f8d65b6e75b540449e92b4886f43607fa02" }, "github/gh-aw-actions/setup@v0.63.0": { "repo": "github/gh-aw-actions/setup", @@ -35,10 +20,15 @@ "version": "v0.63.1", "sha": "53e09ec0be6271e81a69f51ef93f37212c8834b0" }, - "github/gh-aw-actions/setup@v0.64.2": { + "github/gh-aw-actions/setup@v0.64.5": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.64.5", + "sha": "5d2ebfd87a1a45a8a8323c1a12c01b055730dac5" + }, + "github/gh-aw-actions/setup@v0.65.6": { "repo": "github/gh-aw-actions/setup", - "version": "v0.64.2", - "sha": "f22886a9607f5c27e79742a8bfc5faa34737138b" + "version": "v0.65.6", + "sha": "31130b20a8fd3ef263acbe2091267c0aace07e09" } } } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 806f7fad67b045..e7c47261b8ebab 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,8 @@ updates: open-pull-requests-limit: 5 labels: - area-codeflow + exclude-paths: + - ".github/workflows/*.lock.yml" ignore: - dependency-name: "actions/checkout" update-types: ["version-update:semver-patch","version-update:semver-minor"] diff --git a/.github/skills/breaking-change-doc/Build-IssueComment.ps1 b/.github/skills/breaking-change-doc/Build-IssueComment.ps1 new file mode 100644 index 00000000000000..4848f2eaf50e78 --- /dev/null +++ b/.github/skills/breaking-change-doc/Build-IssueComment.ps1 @@ -0,0 +1,94 @@ +# Build-IssueComment.ps1 +# Reads a breaking change issue draft markdown file, URL-encodes the title, +# body, and labels, and produces a PR comment markdown file containing: +# - A header +# - The full draft for inline review +# - A clickable link that pre-fills a new issue in dotnet/docs +# - An email reminder +# +# Usage: +# pwsh .github/skills/breaking-change-doc/Build-IssueComment.ps1 ` +# -IssueDraftPath issue-draft.md ` +# -Title "[Breaking change]: Something changed" ` +# -OutputPath pr-comment.md +# +# The issue draft file should contain only the issue body markdown (no title). + +param( + [Parameter(Mandatory = $true)] + [string]$IssueDraftPath, + + [Parameter(Mandatory = $true)] + [string]$Title, + + [string]$OutputPath = "pr-comment.md", + + [string]$Labels = "breaking-change,Pri1,doc-idea", + + [string]$DocsRepo = "dotnet/docs" +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path $IssueDraftPath)) { + Write-Error "Issue draft file not found: $IssueDraftPath" + exit 1 +} + +$issueBody = Get-Content -Path $IssueDraftPath -Raw -Encoding UTF8 + +# URL-encode using .NET Uri class (same technique the old script used) +$encodedTitle = [Uri]::EscapeDataString($Title) +$encodedBody = [Uri]::EscapeDataString($issueBody) +$encodedLabels = [Uri]::EscapeDataString($Labels) +$encodedEmailSubject = [Uri]::EscapeDataString("[Breaking Change] $Title") + +$issueUrl = "https://github.com/$DocsRepo/issues/new?title=$encodedTitle&body=$encodedBody&labels=$encodedLabels" +$notificationEmailUrl = "mailto:dotnetbcn@microsoft.com?subject=$encodedEmailSubject" + +$comment = @" +## Breaking Change Documentation + +$issueBody + +--- + +> [!NOTE] +> This documentation was generated with AI assistance from Copilot. + +:point_right: **[Click here to create the issue in dotnet/docs]($issueUrl)** + +After creating the issue, please email a link to it to +[.NET Breaking Change Notifications]($notificationEmailUrl). +"@ + +# GitHub comment body limit is 65536 characters. If the comment exceeds this, +# replace the inline draft with a short summary pointing at the file. +$maxCommentLength = 65000 +if ($comment.Length -gt $maxCommentLength) { + Write-Warning "Comment body ($($comment.Length) chars) exceeds GitHub limit. Truncating inline draft." + $comment = @" +## Breaking Change Documentation + +The full draft is too large to display inline. See ``issue-draft.md`` in the +workflow artifacts for the complete content. + +--- + +> [!NOTE] +> This documentation was generated with AI assistance from Copilot. + +:point_right: **[Click here to create the issue in dotnet/docs]($issueUrl)** + +After creating the issue, please email a link to it to +[.NET Breaking Change Notifications]($notificationEmailUrl). +"@ +} + +$comment | Out-File -FilePath $OutputPath -Encoding UTF8 -NoNewline + +Write-Host "Wrote PR comment to $OutputPath ($($comment.Length) characters)" +Write-Host "Issue URL length: $($issueUrl.Length) characters" +if ($issueUrl.Length -gt 8192) { + Write-Warning "URL exceeds 8192 characters. Some browsers may truncate it. Consider shortening the issue body." +} diff --git a/.github/skills/breaking-change-doc/Get-VersionInfo.ps1 b/.github/skills/breaking-change-doc/Get-VersionInfo.ps1 new file mode 100644 index 00000000000000..76332081aa3d38 --- /dev/null +++ b/.github/skills/breaking-change-doc/Get-VersionInfo.ps1 @@ -0,0 +1,196 @@ +# Get-VersionInfo.ps1 +# Determines the .NET version context for a merged PR using the GitHub CLI (gh). +# +# Usage: +# pwsh .github/skills/breaking-change-doc/Get-VersionInfo.ps1 -PrNumber 114929 +# +# Output: JSON object with LastTagBeforeMerge, FirstTagWithChange, EstimatedVersion + +param( + [Parameter(Mandatory = $true)] + [string]$PrNumber, + + [string]$SourceRepo = "dotnet/runtime", + + [string]$BaseRef = "" +) + +$ErrorActionPreference = "Stop" + +function ConvertFrom-DotNetTag { + param([string]$tagName) + + if (-not $tagName -or $tagName -eq "Unknown") { + return $null + } + + if ($tagName -match '^v(\d+)\.(\d+)\.(\d+)(?:-(.+))?$') { + $major = [int]$matches[1] + $minor = [int]$matches[2] + $build = [int]$matches[3] + $prerelease = if ($matches[4]) { $matches[4] } else { $null } + + $prereleaseType = $null + $prereleaseNumber = $null + + if ($prerelease -and $prerelease -match '^([a-zA-Z]+)\.(\d+)') { + $rawType = $matches[1] + $prereleaseNumber = [int]$matches[2] + + if ($rawType -ieq "rc") { + $prereleaseType = "RC" + } else { + $prereleaseType = $rawType.Substring(0, 1).ToUpper() + $rawType.Substring(1).ToLower() + } + } + + return @{ + Major = $major + Minor = $minor + Build = $build + Prerelease = $prerelease + PrereleaseType = $prereleaseType + PrereleaseNumber = $prereleaseNumber + IsRelease = $null -eq $prerelease + } + } + + return $null +} + +function Format-DotNetVersion { + param($parsedTag) + + if (-not $parsedTag) { + return "Next release" + } + + $baseVersion = ".NET $($parsedTag.Major).$($parsedTag.Minor)" + + if ($parsedTag.IsRelease) { + return $baseVersion + } + + if ($parsedTag.PrereleaseType -and $parsedTag.PrereleaseNumber) { + return "$baseVersion $($parsedTag.PrereleaseType) $($parsedTag.PrereleaseNumber)" + } + + return "$baseVersion ($($parsedTag.Prerelease))" +} + +function Get-EstimatedNextVersion { + param($parsedTag, [string]$baseRef) + + if (-not $parsedTag) { + return "Next release" + } + + $isMainBranch = $baseRef -eq "main" + + if ($parsedTag.IsRelease) { + if ($isMainBranch) { + $nextMajor = $parsedTag.Major + 1 + return ".NET $nextMajor.0 Preview 1" + } else { + return ".NET $($parsedTag.Major).$($parsedTag.Minor)" + } + } + + if ($isMainBranch -and $parsedTag.PrereleaseType -eq "RC") { + $nextMajor = $parsedTag.Major + 1 + return ".NET $nextMajor.0 Preview 1" + } else { + $nextPreview = $parsedTag.PrereleaseNumber + 1 + return ".NET $($parsedTag.Major).$($parsedTag.Minor) $($parsedTag.PrereleaseType) $nextPreview" + } +} + +try { + # Step 1: Get PR merge info via GitHub CLI + $prJson = gh pr view $PrNumber --repo $SourceRepo --json mergeCommit,mergedAt,baseRefName 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Failed to fetch PR #$PrNumber from $SourceRepo" + } + $prData = $prJson | ConvertFrom-Json + + $targetCommit = $prData.mergeCommit.oid + $mergedAt = $prData.mergedAt + + if (-not $BaseRef) { + $BaseRef = $prData.baseRefName + } + + # Step 2: Get recent releases (tags with published dates) in a single API call + $releasesJson = gh release list --repo $SourceRepo --limit 100 --json tagName,publishedAt 2>$null + $releases = @() + if ($LASTEXITCODE -eq 0 -and $releasesJson) { + $releases = @($releasesJson | ConvertFrom-Json) + } + + # Filter to .NET version tags (v{major}.{minor}.{patch}[-prerelease]) with a valid publishedAt + $versionReleases = @($releases | Where-Object { $_.tagName -match '^v\d+\.\d+\.\d+' -and $_.publishedAt }) + + $lastTagBefore = "Unknown" + $firstTagWith = "Not yet released" + + if ($mergedAt -and $versionReleases.Count -gt 0) { + $mergedAtDate = [DateTimeOffset]::Parse($mergedAt) + + # Find the most recent release published before the merge + $beforeMerge = @($versionReleases | + Where-Object { [DateTimeOffset]::Parse($_.publishedAt) -lt $mergedAtDate } | + Sort-Object { [DateTimeOffset]::Parse($_.publishedAt) } -Descending) + + if ($beforeMerge.Count -gt 0) { + $lastTagBefore = $beforeMerge[0].tagName + } + + # Find candidate releases published at or after the merge, oldest first + $afterMerge = @($versionReleases | + Where-Object { [DateTimeOffset]::Parse($_.publishedAt) -ge $mergedAtDate } | + Sort-Object { [DateTimeOffset]::Parse($_.publishedAt) }) + + # Verify containment via the compare API: behind_by == 0 means the tag + # includes every commit reachable from the merge commit. + if ($targetCommit -and $afterMerge.Count -gt 0) { + foreach ($release in $afterMerge) { + $tag = $release.tagName + $behindBy = gh api "repos/$SourceRepo/compare/${targetCommit}...${tag}" --jq '.behind_by' 2>$null + if ($LASTEXITCODE -eq 0 -and $behindBy -match '^\d+$' -and [int]$behindBy -eq 0) { + $firstTagWith = $tag + break + } + } + } + } + + # Step 3: Estimate version + $estimatedVersion = "Next release" + + if ($firstTagWith -ne "Not yet released") { + $parsedFirstTag = ConvertFrom-DotNetTag $firstTagWith + $estimatedVersion = Format-DotNetVersion $parsedFirstTag + } else { + $parsedLastTag = ConvertFrom-DotNetTag $lastTagBefore + $estimatedVersion = Get-EstimatedNextVersion $parsedLastTag $BaseRef + } + + # Output as JSON + @{ + LastTagBeforeMerge = $lastTagBefore + FirstTagWithChange = $firstTagWith + EstimatedVersion = $estimatedVersion + MergeCommit = $targetCommit + MergedAt = $mergedAt + BaseRef = $BaseRef + } | ConvertTo-Json + +} catch { + # Return error info as JSON so the agent can handle it + @{ + LastTagBeforeMerge = "Unknown" + FirstTagWithChange = "Not yet released" + EstimatedVersion = "Next release" + Error = $_.Exception.Message + } | ConvertTo-Json +} diff --git a/.github/skills/breaking-change-doc/SKILL.md b/.github/skills/breaking-change-doc/SKILL.md new file mode 100644 index 00000000000000..5a363add915ba3 --- /dev/null +++ b/.github/skills/breaking-change-doc/SKILL.md @@ -0,0 +1,307 @@ +--- +name: breaking-change-doc +description: > + Generate breaking change documentation for merged dotnet/runtime PRs. + USE FOR: creating breaking change docs, "document this breaking change", + "write breaking change issue for PR #NNNNN", processing PRs labeled + needs-breaking-change-doc-created. DO NOT USE FOR: general code review + (use code-review skill), bug fixes, API proposals (use api-proposal skill). +--- + +# Breaking Change Documentation Skill + +Generate high-quality breaking change documentation for merged dotnet/runtime +pull requests and file it as an issue in [dotnet/docs](https://github.com/dotnet/docs). + +## Overview + +When a PR in dotnet/runtime introduces a breaking change, the docs team needs +a structured issue in dotnet/docs describing the change, its impact, and +migration guidance. This skill automates that process: + +1. **Gather PR context** — read the PR, its diff, related issues, comments, and reviews. +2. **Detect version** — run the helper script to determine which .NET release the change lands in. +3. **Check for duplicates** — search dotnet/docs for existing breaking-change issues for this PR. +4. **Fetch reference material** — read the issue template and recent example issues from dotnet/docs. +5. **Author the documentation** — produce the issue body following the template structure and example quality. +6. **Publish** — write output files and optionally comment on the source PR. + +### Trigger modes + +- **Interactive**: Ask Copilot (e.g. "Document the breaking change + in PR #114929"). The skill presents a draft for review before publishing. +- **Automated**: The [GitHub Agentic Workflow](https://github.github.com/gh-aw/) + at `.github/workflows/breaking-change-doc.md` triggers when a PR labeled + `needs-breaking-change-doc-created` is merged (or the label is added to an + already-merged PR). It can also be run manually via `workflow_dispatch` with + an optional `suppress_output` flag for dry-run inspection. The compiled + workflow (`.lock.yml`) must be regenerated with `gh aw compile` after changes. + +### Files + +| File | Purpose | +|------|---------| +| `.github/workflows/breaking-change-doc.md` | gh-aw workflow — triggers on PR merge/label | +| `.github/workflows/breaking-change-doc.lock.yml` | Compiled workflow (generated by `gh aw compile`) | +| `.github/skills/breaking-change-doc/SKILL.md` | This skill | +| `.github/skills/breaking-change-doc/Get-VersionInfo.ps1` | `gh` CLI-based .NET version detection | +| `.github/skills/breaking-change-doc/Build-IssueComment.ps1` | Builds PR comment with URL-encoded issue creation link | + +--- + +## Step 0: Accept Input + +The user provides one of: +- A PR number (e.g. `#114929` or `114929`) +- A PR URL (e.g. `https://github.com/dotnet/runtime/pull/114929`) +- A request like "document the breaking change in PR 114929" + +Extract the PR number. The source repository is always `dotnet/runtime`. + +--- + +## Step 1: Gather PR Context + +Use GitHub tools to read comprehensive PR data. Collect **all** of the following: + +1. **PR metadata**: title, author, base branch, merge commit SHA, merged-at date, labels, state. +2. **PR body**: the full description. +3. **Changed files**: list of file paths modified. +4. **PR comments and reviews**: read all comments and review comments for context about the change's impact. +5. **Closing issues**: if the PR closes any issues, read those issues fully (body + comments) — they often contain the motivation and user-reported impact. +6. **Feature area labels**: extract `area-*` labels. If none exist, report an error asking the user to set one. + +### Identifying the feature area + +Map the `area-*` label to the dotnet/docs feature area dropdown value: + +| `area-*` label pattern | Feature area | +|---|---| +| `area-System.Net.*`, `area-Networking` | Networking | +| `area-System.Security.*`, `area-Cryptography` | Cryptography | +| `area-System.Text.Json`, `area-Serialization` | Serialization | +| `area-System.Xml.*` | XML, XSLT | +| `area-Extensions-*` | Extensions | +| `area-System.Globalization` | Globalization | +| `area-System.Runtime.InteropServices*`, `area-Interop` | Interop | +| `area-CodeGen-*`, `area-JIT` | JIT | +| `area-System.Linq*` | LINQ | +| `area-System.CodeDom`, `area-Analyzers` | Code analysis | +| `area-Infrastructure-*`, `area-SDK` | SDK | +| `area-System.Windows.Forms*` | Windows Forms | +| `area-WPF` | Windows Presentation Foundation (WPF) | +| Most other `area-System.*` labels | Core .NET libraries | + +If the mapping is unclear, use "Other (please put exact area in description textbox)" and +include the actual area label in the description. + +--- + +## Step 2: Detect Version Information + +Run the helper script to determine the .NET version context: + +``` +pwsh .github/skills/breaking-change-doc/Get-VersionInfo.ps1 -PrNumber +``` + +The script outputs JSON with: +- `LastTagBeforeMerge` — the closest release tag before the merge commit +- `FirstTagWithChange` — the first tag that contains this commit (or "Not yet released") +- `EstimatedVersion` — human-readable version string like ".NET 11 Preview 3" +- `MergeCommit` — the merge commit SHA +- `MergedAt` — when the PR was merged + +Use `EstimatedVersion` as the version for the breaking change issue. + +If the script returns an error, fall back to reading the PR's base branch and recent +tags manually to estimate the version. + +--- + +## Step 3: Check for Existing Documentation + +Search for existing breaking change issues in dotnet/docs: + +- Search `dotnet/docs` issues for `Breaking change ` with label `breaking-change`. +- If a matching issue already exists, report it to the user and **stop** — do not create a duplicate. + +--- + +## Step 4: Fetch Reference Material + +### Issue template + +Read the breaking change issue template from dotnet/docs at: +`.github/ISSUE_TEMPLATE/02-breaking-change.yml` + +``` +gh api repos/dotnet/docs/contents/.github/ISSUE_TEMPLATE/02-breaking-change.yml -H "Accept: application/vnd.github.raw" +``` + +This template defines the required sections and dropdown values. Use it as a +structural reference only — do **not** output YAML. + +### Example issues + +Search `dotnet/docs` for 2-3 recent issues with the `breaking-change` label. +Read their bodies to understand the expected quality, tone, and level of detail. + +--- + +## Step 5: Author the Documentation + +Generate a complete breaking change issue. The output must be **clean markdown** +formatted to work with the GitHub issue form template. Structure it as follows: + +### Required sections + +#### Title +`[Breaking change]: ` + +Do not just repeat the PR title. Write a clear, user-facing summary. + +#### Description +Brief description of the breaking change. Include the PR link. + +#### Version +Use the `EstimatedVersion` from Step 2. Must match one of the template dropdown values +(e.g., ".NET 11 Preview 3"). If it doesn't match exactly, use +"Other (please put exact version in description textbox)" and state the version in the description. + +#### Previous behavior +Describe what happened before the change. Include a **code example** if applicable +showing the old behavior. + +#### New behavior +Describe what happens now. Include a **code example** if applicable showing the +new behavior. Highlight exceptions thrown, changed return values, or different +default settings. + +#### Type of breaking change +Categorize as one or more of: +- **Binary incompatible**: existing binaries may fail to load/execute +- **Source incompatible**: existing source may fail to compile +- **Behavioral change**: existing binaries behave differently at runtime + +Most changes are behavioral. Only mark binary/source incompatible when the +change actually affects compilation or binary loading. + +#### Reason for change +Explain **why** the change was made. Reference the motivation from the PR body +and closing issues. + +#### Recommended action +Provide **specific, actionable** guidance: +- Code changes the user should make +- Configuration switches to restore old behavior (if any exist, e.g. AppContext switches) +- Workarounds + +#### Feature area +Use the mapping from Step 1. + +#### Affected APIs +List all affected APIs. For methods, specify whether it's all overloads or +specific ones. Use fully qualified names (e.g., `System.IO.Compression.ZipArchiveEntry.Open()`). + +### Quality guidelines + +- **Professional tone** — this is official Microsoft documentation. +- **Concrete examples** — before/after code snippets make the change tangible. +- **Actionable guidance** — don't just describe the problem, help users fix it. +- **Accurate version** — use the detected version from Step 2. +- **Complete API list** — review the diff to find all affected public APIs. + +--- + +## Step 6: Publish + +### Write the issue draft file + +Create the output directory and write the full markdown content from Step 5 +(everything except the title line) to +`artifacts/docs/breakingChanges/issue-draft.md`. This file is the primary +output and can be reviewed before taking any further action. + +```bash +mkdir -p artifacts/docs/breakingChanges +``` + +### Build the PR comment file + +Run the helper script to produce a PR comment with a URL-encoded issue link: + +```bash +pwsh .github/skills/breaking-change-doc/Build-IssueComment.ps1 \ + -IssueDraftPath artifacts/docs/breakingChanges/issue-draft.md \ + -Title "" \ + -OutputPath artifacts/docs/breakingChanges/pr-comment.md +``` + +The script: +- URL-encodes the title, body, and labels using `[Uri]::EscapeDataString` +- Builds a clickable `https://github.com/dotnet/docs/issues/new?...` link +- Writes `pr-comment.md` containing the full draft, the link, and an email reminder +- Warns if the URL exceeds browser length limits + +### Post the comment + +When running interactively (not in dry-run mode), post the contents of +`artifacts/docs/breakingChanges/pr-comment.md` as a comment on the original +dotnet/runtime PR using GitHub tools. + +When running in automated (gh-aw) mode, use the add_comment safe-output tool to post the comment. + +When in dry-run mode or when the user has not explicitly asked to comment, +skip posting — the two files under `artifacts/docs/breakingChanges/` are the +outputs for review. + +### AI-generated content disclosure + +The `Build-IssueComment.ps1` script includes the standard AI disclosure note +in the generated comment. No additional action is needed. + +### Email reminder + +The generated comment includes an email reminder. When running interactively, +also remind the user: +> Please email a link to this breaking change issue to +> [.NET Breaking Change Notifications](mailto:dotnetbcn@microsoft.com). + +--- + +## Draft-only mode + +If the user asks for a draft or review before publishing, or if you are uncertain +about any aspect of the documentation: + +1. Present the full issue content in chat for review. +2. Ask the user to confirm before creating the issue. +3. Only publish after explicit confirmation. + +When the user has not explicitly asked to create the issue, default to draft mode. + +--- + +## Processing multiple PRs + +If the user provides a GitHub search query or asks to process multiple PRs: + +1. Search for matching PRs using the query. +2. For each PR, run Steps 1-5. +3. Present a summary table of all PRs with their status (already documented vs needs docs). +4. For PRs needing docs, present drafts and ask for confirmation before creating issues. + +--- + +## Troubleshooting + +| Problem | Solution | +|---|---| +| No `area-*` label on PR | Ask the user to add one, or ask which area applies | +| Version detection script fails | Read the PR's base branch and use `git describe --tags --abbrev=0` manually | +| PR not yet merged | Breaking change docs are for merged PRs only — inform the user | +| Existing docs issue found | Report the existing issue URL and stop | +| Cannot determine affected APIs | Review the diff carefully; list the public types/methods in changed files | +| Workflow not triggering | Ensure `gh aw compile` was run and `.lock.yml` is committed | +| AI output needs review | Check technical accuracy of before/after behavior, API list completeness, and migration guidance | diff --git a/.github/skills/update-os-coverage/SKILL.md b/.github/skills/update-os-coverage/SKILL.md new file mode 100644 index 00000000000000..35a6fd28f95e51 --- /dev/null +++ b/.github/skills/update-os-coverage/SKILL.md @@ -0,0 +1,306 @@ +--- +name: update-os-coverage +description: > + Update OS version references in Helix queue definitions to add new versions, + replace EOL versions, or audit coverage against the supported-os matrix. + USE FOR: adding new OS versions to Helix queues, replacing EOL OS versions, + upgrading "oldest" or "latest" version references, auditing Helix coverage. + DO NOT USE FOR: creating new container images (that's dotnet-buildtools-prereqs-docker), + updating supported-os.json (that's the update-supported-os skill in dotnet/core). +--- + +# Update OS Coverage + +Update OS version references in Helix queue definition files. These files control which operating system versions are used for CI/CD testing via Helix. + +## Prerequisites + +> **Baseline build not required:** This skill is for YAML/docs-style queue and image reference updates, not product code changes. Do **not** start with the repo-wide baseline build workflow from [`copilot-instructions.md`](/.github/copilot-instructions.md) unless the task expands beyond image / queue metadata into code changes that actually need build or test validation. + +## When to use + +- An OS version is approaching or has reached EOL and should be replaced +- A new OS version is released and should be added to Helix testing for coverage +- We take a more proactive approach on `main`. If a distro version will be EOL before our annual November release, we should update it to a newer version. If a distro version is expected to ship within one quarter (3 months) and a `prereqs` container image already exists, we should add it to Helix testing. +- The availability of an image in the `prereqs` container repo is a strong signal that the OS version is approved for Helix testing, at least on `main`. +- Helix coverage does not match the supported-os matrix (for example, the relevant `release-notes//supported-os.json` file in [dotnet/core](https://github.com/dotnet/core/tree/main/release-notes)). +- Upgrading "oldest" or "latest" version slots for a distro + +For servicing / `release/*` branches, be more conservative: only update to GA and already-supported distro versions unless the user explicitly asks for a forward-looking change. + +## When NOT to use + +- Creating new container images → file an issue or PR at [dotnet-buildtools-prereqs-docker](https://github.com/dotnet/dotnet-buildtools-prereqs-docker) +- Updating `supported-os.json` / `supported-os.md` → file an issue in [dotnet/core](https://github.com/dotnet/core) +- Adding entirely new distros or architectures to Helix (requires pipeline template changes beyond version bumps) +- Requesting new Helix VM queues → file an issue at [dotnet/dnceng](https://github.com/dotnet/dnceng) +- Updating Windows or macOS Helix queues — these use VM-based queues with a simpler format (e.g. `Windows.11.Amd64.Client.Open`) and version updates typically require dnceng coordination + +## Key files + +OS version references appear in these pipeline files: + +| File | Purpose | +|------|---------| +| `eng/pipelines/helix-platforms.yml` | Central platform definitions — a useful starting point for many `latest` and `oldest` OS version variables, but not the sole source of truth | +| `eng/pipelines/libraries/helix-queues-setup.yml` | Libraries Helix queue assignments — inline OS version references per platform | +| `eng/pipelines/coreclr/templates/helix-queues-setup.yml` | CoreCLR Helix queue assignments — inline OS version references | +| `eng/pipelines/installer/helix-queues-setup.yml` | Installer Helix queue assignments | +| `eng/pipelines/common/templates/pipeline-with-resources.yml` | Build container definitions (not Helix queues, but OS version references for build images) | +| `docs/workflow/using-docker.md` | Documents the official build/test Docker images — update only when build image versions change (cross-compilation images, not Helix test images) | + +The [OS onboarding guide](/docs/project/os-onboarding.md) is the authoritative reference for how OS versions are managed in this repo. Read it if more context is needed on our policies. + +### helix-platforms.yml structure + +Many Linux container-backed entries in this file use a pattern like: + +```yaml +# +# Latest: +- name: helix_linux_x64__latest + value: ()@mcr.microsoft.com/dotnet-buildtools/prereqs: +``` + +Where `` is the Helix queue identifier (e.g. `Fedora.44.Amd64.Open`), `` is the physical host queue (e.g. `AzureLinux.3.Amd64.Open`), and `` is the container image tag (e.g. `fedora-44-helix-amd64`). + +Other entries in the same file are plain queue values (for example Windows, macOS, and some Linux VM queues) rather than `()@`. When the target entry is queue-only, preserve that format and update only the versioned queue string. + +Some platform variables have `_internal` counterparts (e.g. `helix_linux_x64_oldest_internal`, `helix_linux_musl_arm32_latest_internal`) that use the same queue/image but drop the `.Open` suffix. When an `_internal` counterpart exists, update both the `.Open` and `_internal` entries. + +### helix-queues-setup.yml files + +These files reference OS versions directly (not via variables) in conditional blocks per platform. Most Linux container-backed inline references follow the `()@` format, while a few entries are plain queue values (for example, some AzureLinux-only queues). Preserve the existing format for the specific entry you are updating. + +## Inputs + +The user provides one or more of: + +- **Distro and version** — e.g. "Update Fedora to 44", "Replace Alpine 3.20 with 3.22" +- **Slot** — whether to update `latest`, `oldest`, or both +- **Branch** — defaults to current branch; may also need release branch updates +- **Audit mode** — "check all OS versions against supported-os.json" + +If the user provides only a distro name without specifying slots, either ask or determine with basic logic which slots to update (for example, if the current `latest` is EOL, update `latest`; if the current `oldest` is EOL, update `oldest`). + +## Process + +Use the repo tools that fit the environment. The shell snippets below are reference commands, not a required literal script; equivalent `gh`, `git`, or search-based workflows are fine. + +### 1. Verify container image availability + +Before making any changes, confirm the **exact target container tag** exists in the [image-info JSON](https://github.com/dotnet/versions/blob/main/build-info/docker/image-info.dotnet-dotnet-buildtools-prereqs-docker-main.json). This file is more authoritative than probing the registry directly and should be the primary source of truth for published `dotnet-buildtools/prereqs` tags: + +```bash +TARGET_TAG="" +curl -sL https://github.com/dotnet/versions/raw/refs/heads/main/build-info/docker/image-info.dotnet-dotnet-buildtools-prereqs-docker-main.json \ + | jq -r --arg tag "$TARGET_TAG" '[.repos[].images[].platforms[].simpleTags[]] | unique | map(select(. == $tag)) | .[]' +``` + +If the exact tag is **not found in `image-info`**, stop and inform the user. Treat that as authoritative even if a registry lookup appears to work. The image must be created first at [dotnet/dotnet-buildtools-prereqs-docker](https://github.com/dotnet/dotnet-buildtools-prereqs-docker). Check if an open issue or PR already exists, for example: + +```bash +gh search issues " " --repo dotnet/dotnet-buildtools-prereqs-docker --state open +``` + +### 2. Check support policy first, then EOL dates if needed + +First, inspect the relevant `supported-os.json` entry in `dotnet/core` to see whether the distro/version is already supported for the target release and to find its official lifecycle link: + +```bash +curl -sL https://github.com/dotnet/core/raw/refs/heads/main/release-notes//supported-os.json \ + | jq '.families[] | select(.name == "Linux") | .distributions[] | select(.id == "") | {name, lifecycle, supportedVersions: ."supported-versions", unsupportedVersions: ."unsupported-versions"}' +``` + +If the target distro version is already listed in `supportedVersions`, that is the primary signal that the change is appropriate for the corresponding release line. On servicing branches, prefer versions that are already GA and present there. + +If you need an independent lifecycle check, or if `supported-os.json` does not yet reflect the situation clearly, use [endoflife.date](https://endoflife.date) as a fallback: + +```bash +curl -s https://endoflife.date/api/.json | jq '.[] | select(.cycle == "") | {cycle, eol, releaseDate}' +``` + +The `` values typically match across both sources (e.g. `fedora`, `alpine`, `debian`, `opensuse`, `ubuntu`, `centos-stream`). + +### 3. Scan current references + +Search for all current references to the distro being updated. For example: + +```bash +grep -rn -i "" \ + eng/pipelines/helix-platforms.yml \ + eng/pipelines/libraries/helix-queues-setup.yml \ + eng/pipelines/coreclr/templates/helix-queues-setup.yml \ + eng/pipelines/installer/helix-queues-setup.yml \ + eng/pipelines/common/templates/pipeline-with-resources.yml \ + docs/workflow/using-docker.md +``` + +Note every occurrence — the same distro may appear in multiple sections (x64, arm32, arm64) and in multiple files. + +### 4. Apply changes + +For each reference found in step 3: + +1. **Start with `helix-platforms.yml`** — it is a convenient central catalog for many Linux container-backed entries, but it is not the source of truth by itself + - Update the version comment (e.g. `# Latest: 43` → `# Latest: 44`) + - Update the variable value — adjust the queue name and image tag to use the new version when the entry uses the container-backed format + - Preserve the existing host queue (e.g. `AzureLinux.3.Amd64.Open`) — this does not change with distro version updates + - Then continue through the other files until every matching reference is updated consistently + +2. **Update `helix-queues-setup.yml` files** — libraries, coreclr, and installer templates + - Search for inline references to the old version and update them + - These are direct queue strings, not variable references + +3. **Version naming conventions** — follow existing patterns exactly: + + | Distro | Queue name pattern | Image tag pattern | + |--------|--------------------|-------------------| + | Alpine | `Alpine..Amd64.Open` | `alpine--helix-amd64` | + | Alpine (edge) | `Alpine.edge.Amd64.Open` (casing varies by file; see note below) | `alpine-edge-helix-amd64` | + | CentOS Stream | `Centos..Amd64.Open` | `centos-stream--helix-amd64` | + | Debian | `Debian..Amd64.Open` | `debian--helix-amd64` | + | Fedora | `Fedora..Amd64.Open` | `fedora--helix-amd64` | + | openSUSE | `openSUSE..Amd64.Open` | `opensuse--helix-amd64` | + | Ubuntu | `Ubuntu..Amd64.Open` | `ubuntu--helix-amd64` | + + For Alpine edge queues, the casing of `edge` is not consistent across the repo (`Alpine.edge.Amd64.Open` in `helix-platforms.yml` vs `Alpine.Edge.Amd64.Open` in `libraries/helix-queues-setup.yml`). When updating queue strings, **preserve the existing casing used in each file** rather than normalizing to a single pattern. + + AzureLinux (e.g. `AzureLinux.3.Amd64.Open`) appears as both a standalone VM queue and the primary host queue for container-based distros. It does not follow the container image pattern above. + + Architecture suffixes vary: `Amd64`, `Arm64`, `ArmArch`, `Arm32` for queue names; `amd64`, `arm64v8`, `arm32v7` for image tags. + + When both generic and processor-specific aliases exist in `image-info` (for example, `ubuntu-26.04-helix-webassembly` and `ubuntu-26.04-helix-webassembly-amd64`), **prefer the processor-specific tag** when the queue/environment is processor-specific: + + - `...Amd64...` queue → prefer `*-amd64` + - `...Arm64...` / `...ArmArch...` queue → prefer `*-arm64v8` + - `...Arm32...` queue → prefer `*-arm32v7` + + Use the generic alias only when the surrounding environment is intentionally architecture-agnostic or when no processor-specific tag exists in `image-info`. + + For ARM-based queues, host queues are often `Ubuntu.2204.ArmArch.Open`, but some queues (for example `helix_linux_arm64_oldest`) use AzureLinux-based host queues such as `AzureLinux.3.Arm64.Open`. Follow the existing pattern for the specific queue in `eng/pipelines/helix-platforms.yml` when updating versions. + +### 5. Validate changes + +After editing, verify: + +1. **No stale references remain** — re-run the grep from step 3, replacing the distro name with the old version pattern. Stale references are acceptable only if intentionally kept (e.g. the version is still used for `oldest`). + +2. **All new references are syntactically consistent** — compare with adjacent entries in the same file to verify formatting. + +3. **Updated image tags are present in `image-info`** — verify that each new tag you used appears in `image-info.dotnet-dotnet-buildtools-prereqs-docker-main.json`. + +4. **Variable names are unchanged** — only the `value` fields change, never the `name` fields. + +### 6. CI pipeline coverage + +Not all Helix queues run in the default PR pipeline (`runtime`). Some distros are only exercised by the **extra-platforms** pipeline (`runtime-extra-platforms`), which runs on a daily schedule and can be triggered on PRs with `/azp run runtime-extra-platforms`. + +**Distros behind the `isExtraPlatformsBuild` guard** — generally need `runtime-extra-platforms` for coverage that is not already exercised by the default `runtime` pipeline: + +| Distro | Platform | File | +|--------|----------|------| +| Debian* | linux_x64, linux_arm | `libraries/helix-queues-setup.yml` | +| Fedora | linux_x64 | `libraries/helix-queues-setup.yml` | +| openSUSE | linux_x64 | `libraries/helix-queues-setup.yml` | +| Alpine edge | linux_musl_x64 | `libraries/helix-queues-setup.yml` | +| Ubuntu | linux_arm64 | `libraries/helix-queues-setup.yml` | +| Alpine (versioned) | linux_musl_arm64 | `libraries/helix-queues-setup.yml` | + +> **Note:** Debian `linux_x64` is also exercised in the default `runtime` pipeline for some libraries scenarios (for example, when `interpreter: true` or `isSingleFile: true`). Use `runtime-extra-platforms` when you need Debian coverage outside those default-pipeline cases; Debian `linux_arm` remains extra-platforms-only here. + +**Distros in the default PR pipeline** — no extra pipeline needed: + +| Distro | Platform | File | +|--------|----------|------| +| Ubuntu | linux_x64 | `libraries/helix-queues-setup.yml` | +| AzureLinux | linux_x64, linux_arm64 | `libraries/helix-queues-setup.yml`, `coreclr/templates/helix-queues-setup.yml` | +| CentOS Stream | linux_x64 | `libraries/helix-queues-setup.yml` | +| Alpine (versioned) | linux_musl_x64, linux_musl_arm64 | `libraries/helix-queues-setup.yml`, `coreclr/templates/helix-queues-setup.yml` | + +> **Note:** Alpine versioned linux_musl_arm64 is behind `isExtraPlatformsBuild` in `libraries/helix-queues-setup.yml`, but runs unconditionally in `coreclr/templates/helix-queues-setup.yml`. It is listed here because coreclr provides default pipeline coverage. + +**Decision:** After creating a PR, cross-reference the distro/platform combinations you changed against the tables above. Trigger `runtime-extra-platforms` when you changed coverage that is only, or primarily, exercised by the first table. If every changed distro/platform is already covered by the default `runtime` pipeline, no extra pipeline run is needed. + +When extra-platforms is needed, post the trigger comment on the PR: + +```bash +gh pr comment --body "/azp run runtime-extra-platforms" +``` + +Tell the user you've triggered the pipeline and which distros required it. + +> **Note:** The `outerloop` pipeline (`libraries/outerloop.yml`) does not add Linux distro coverage beyond the default pipeline for normal PR validation. The extra-platforms distros (Fedora, Debian, openSUSE) are only brought in for specific non-PR / rolling-build-style cases there, so **do not** trigger outerloop for Linux distro version changes unless the user explicitly requests it. + +### 7. Check other branches + +After updating `main`, check whether release branches also need updates: + +```bash +for branch in $(git branch -r | grep -E 'origin/release/'); do + echo "=== $branch ===" + git show "$branch:eng/pipelines/helix-platforms.yml" 2>/dev/null | grep -n -i "" || echo "(no matches or file not found)" +done +``` + +> **Note**: In shallow clones, remote release branches may not be visible. Run `git fetch origin 'refs/heads/release/*:refs/remotes/origin/release/*'` first, or check release branches via GitHub. + +Release branches should be updated when: +- The old version is EOL or approaching EOL +- The release branch will be serviced for longer than the old version's remaining support + +On servicing branches, prefer GA and already-supported distro versions. Do not pre-stage not-yet-GA or merely upcoming versions there unless the user explicitly requests it. + +Note: Release branch updates should be done in separate PRs. + +### 8. Cross-reference with supported-os + +Check if the relevant `supported-os.json` in dotnet/core needs corresponding updates. The file lives under the pattern `release-notes//supported-os.json` (for example, `release-notes/8.0/supported-os.json`) in the [release-notes directory](https://github.com/dotnet/core/tree/main/release-notes). If a new distro version is being added to Helix but isn't yet in supported-os, inform the user to run the `update-supported-os` skill in dotnet/core. + +### 9. Create PR + +The PR description should include: + - Table of changes (old version → new version, which slots) + - EOL dates for old and new versions + - Confirmation that the exact container image tags are available in `image-info.dotnet-dotnet-buildtools-prereqs-docker-main.json` + - Which CI pipeline(s) need to run (see [step 6](#6-ci-pipeline-coverage)) + - Link to the [os-onboarding guide](https://github.com/dotnet/runtime/blob/main/docs/project/os-onboarding.md) + - Link to tracking issue if applicable (e.g. [dotnet/core#9638](https://github.com/dotnet/core/issues/9638)) + +> 📝 **AI-generated content disclosure:** When posting any content to GitHub (PR descriptions, comments) under a user's credentials — i.e., the account is **not** a dedicated "copilot" or "bot" account/app — you **MUST** include a concise, visible note (e.g. a `> [!NOTE]` alert) indicating the content was AI/Copilot-generated. Skip this if the user explicitly asks you to omit it. + +## Audit mode + +When asked to audit all OS coverage: + +1. Fetch the current supported-os.json for the target .NET version: + ```bash + curl -sL https://github.com/dotnet/core/raw/refs/heads/main/release-notes//supported-os.json + ``` + +2. For each Linux distro in supported-os, check: + - The `latest` version in `helix-platforms.yml` should be the newest supported version + - The `oldest` version should be the oldest still-supported version + - No EOL versions should remain + +3. Check EOL dates for all referenced versions: + ```bash + curl -s https://endoflife.date/api/.json | jq '[.[] | select(.eol != false) | select(.eol < "YYYY-MM-DD")] | .[].cycle' + ``` + +4. Report findings as a table: + + | Distro | Slot | Current | Recommended | Reason | + |--------|------|---------|-------------|--------| + | Fedora | latest | 43 | 44 | 44 now GA | + | Fedora | oldest | 42 | 43 | 42 EOL 2026-05-13 | + +## Reference + +- [OS onboarding guide](/docs/project/os-onboarding.md) +- [.NET OS Support Tracking](https://github.com/dotnet/core/issues/9638) +- [Prereq container image lifecycle](https://github.com/dotnet/dotnet-buildtools-prereqs-docker/blob/main/lifecycle.md) +- [Container image registry (image-info)](https://github.com/dotnet/versions/blob/main/build-info/docker/image-info.dotnet-dotnet-buildtools-prereqs-docker-main.json) +- [endoflife.date](https://endoflife.date/) for OS lifecycle data +- [PR #125991](https://github.com/dotnet/runtime/pull/125991) — example EOL OS version replacement +- [PR #111768](https://github.com/dotnet/runtime/pull/111768) — example new OS version onboarding diff --git a/.github/workflows/breaking-change-doc.lock.yml b/.github/workflows/breaking-change-doc.lock.yml new file mode 100644 index 00000000000000..92c97cc4f74f2c --- /dev/null +++ b/.github/workflows/breaking-change-doc.lock.yml @@ -0,0 +1,1194 @@ +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.65.6). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Generate breaking change documentation for merged PRs labeled needs-breaking-change-doc-created. Produces two markdown files (issue-draft.md and pr-comment.md) and optionally comments on the PR. +# +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7c86aef08973645499a88ff9ac23e1719f681df3b8dc13c01af6fb3fae9ca83c","compiler_version":"v0.65.6","strict":true,"agent_id":"copilot"} + +name: "Breaking Change Documentation" +"on": + pull_request_target: + types: + - closed + - labeled + # steps: # Steps injected into pre-activation job + # - name: Checkout the select-copilot-pat action folder + # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + # with: + # fetch-depth: 1 + # persist-credentials: false + # sparse-checkout: .github/actions/select-copilot-pat + # sparse-checkout-cone-mode: true + # - env: + # SECRET_0: ${{ secrets.COPILOT_PAT_0 }} + # SECRET_1: ${{ secrets.COPILOT_PAT_1 }} + # SECRET_2: ${{ secrets.COPILOT_PAT_2 }} + # SECRET_3: ${{ secrets.COPILOT_PAT_3 }} + # SECRET_4: ${{ secrets.COPILOT_PAT_4 }} + # SECRET_5: ${{ secrets.COPILOT_PAT_5 }} + # SECRET_6: ${{ secrets.COPILOT_PAT_6 }} + # SECRET_7: ${{ secrets.COPILOT_PAT_7 }} + # SECRET_8: ${{ secrets.COPILOT_PAT_8 }} + # SECRET_9: ${{ secrets.COPILOT_PAT_9 }} + # id: select-copilot-pat + # name: Select Copilot token from pool + # uses: ./.github/actions/select-copilot-pat + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + pr_number: + description: Pull Request Number + required: true + type: string + suppress_output: + default: false + description: Suppress workflow output (dry-run — only produce markdown workflow artifacts) + required: false + type: boolean + +permissions: {} + +concurrency: + cancel-in-progress: true + group: breaking-change-doc-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }} + +run-name: "Breaking Change Documentation" + +jobs: + activation: + needs: pre_activation + if: > + needs.pre_activation.outputs.activated == 'true' && (github.event_name == 'workflow_dispatch' || + ( + !github.event.repository.fork && + github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'needs-breaking-change-doc-created') + )) + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + body: ${{ steps.sanitized.outputs.body }} + comment_id: "" + comment_repo: "" + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + text: ${{ steps.sanitized.outputs.text }} + title: ${{ steps.sanitized.outputs.title }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "latest" + GH_AW_INFO_AGENT_VERSION: "latest" + GH_AW_INFO_CLI_VERSION: "v0.65.6" + GH_AW_INFO_WORKFLOW_NAME: "Breaking Change Documentation" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.11" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: ${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "breaking-change-doc.lock.yml" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_COMPILED_VERSION: "v0.65.6" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Compute current body text + id: sanitized + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_INPUTS_PR_NUMBER: ${{ github.event.inputs.pr_number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh + { + cat << 'GH_AW_PROMPT_f46a753a06b313f7_EOF' + + GH_AW_PROMPT_f46a753a06b313f7_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_f46a753a06b313f7_EOF' + + Tools: add_comment, missing_tool, missing_data, noop + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_f46a753a06b313f7_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_f46a753a06b313f7_EOF' + + {{#runtime-import .github/workflows/breaking-change-doc.md}} + GH_AW_PROMPT_f46a753a06b313f7_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_EVENT_INPUTS_PR_NUMBER: ${{ github.event.inputs.pr_number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_INPUTS_PR_NUMBER: ${{ github.event.inputs.pr_number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_INPUTS_PR_NUMBER: process.env.GH_AW_GITHUB_EVENT_INPUTS_PR_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash ${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash ${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: breakingchangedoc + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Set runtime paths + id: set-runtime-paths + run: | + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash ${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure gh CLI for GitHub Enterprise + run: bash ${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh + env: + GH_TOKEN: ${{ github.token }} + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest + - name: Install AWF binary + run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.11 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.11 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.11 ghcr.io/github/gh-aw-firewall/squid:0.25.11 ghcr.io/github/gh-aw-mcpg:v0.2.11 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_e4f1701b94f3ff9c_EOF' + {"add_comment":{"max":1,"target":"*"},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"}} + GH_AW_SAFE_OUTPUTS_CONFIG_e4f1701b94f3ff9c_EOF + - name: Write Safe Outputs Tools + run: | + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_aa956acbe39a3949_EOF' + { + "description_suffixes": { + "add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: *." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_SAFE_OUTPUTS_TOOLS_META_aa956acbe39a3949_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_65e8725d06d4acd6_EOF' + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + } + } + GH_AW_SAFE_OUTPUTS_VALIDATION_65e8725d06d4acd6_EOF + node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash ${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.11' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_443fee0fd0da6a12_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_443fee0fd0da6a12_EOF + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool shell(cat) + # --allow-tool shell(date) + # --allow-tool shell(echo) + # --allow-tool shell(gh:*) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(ls) + # --allow-tool shell(pwd) + # --allow-tool shell(pwsh) + # --allow-tool shell(sort) + # --allow-tool shell(tail) + # --allow-tool shell(uniq) + # --allow-tool shell(wc) + # --allow-tool shell(yq) + # --allow-tool write + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.11 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(gh:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(pwsh)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.65.6 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash ${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash ${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/parse_token_usage.sh + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - if: always() + name: Upload breaking change drafts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + if-no-files-found: ignore + name: breaking-change-docs + path: artifacts/docs/breakingChanges/ + retention-days: 30 + + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + if-no-files-found: ignore + - name: Upload firewall audit logs + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: firewall-audit-logs + path: | + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-breaking-change-doc" + cancel-in-progress: false + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Breaking Change Documentation" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Breaking Change Documentation" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Breaking Change Documentation" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "breaking-change-doc" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_TIMEOUT_MINUTES: "20" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Download container images + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.11 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.11 ghcr.io/github/gh-aw-firewall/squid:0.25.11 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + WORKFLOW_NAME: "Breaking Change Documentation" + WORKFLOW_DESCRIPTION: "Generate breaking change documentation for merged PRs labeled needs-breaking-change-doc-created. Produces two markdown files (issue-draft.md and pr-comment.md) and optionally comments on the PR." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Install GitHub Copilot CLI + run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest + - name: Install AWF binary + run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.11 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.11 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.65.6 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + + pre_activation: + if: > + github.event_name == 'workflow_dispatch' || + ( + !github.event.repository.fork && + github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'needs-breaking-change-doc-created') + ) + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} + matched_command: '' + select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + await main(); + - name: Checkout the select-copilot-pat action folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + sparse-checkout: .github/actions/select-copilot-pat + sparse-checkout-cone-mode: true + - name: Select Copilot token from pool + id: select-copilot-pat + uses: ./.github/actions/select-copilot-pat + env: + SECRET_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_9: ${{ secrets.COPILOT_PAT_9 }} + + safe_outputs: + needs: + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/breaking-change-doc" + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_WORKFLOW_ID: "breaking-change-doc" + GH_AW_WORKFLOW_NAME: "Breaking Change Documentation" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} + comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Output Items + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-output-items + path: /tmp/gh-aw/safe-output-items.jsonl + if-no-files-found: ignore + diff --git a/.github/workflows/breaking-change-doc.md b/.github/workflows/breaking-change-doc.md new file mode 100644 index 00000000000000..3abd66915e76a8 --- /dev/null +++ b/.github/workflows/breaking-change-doc.md @@ -0,0 +1,142 @@ +--- +description: > + Generate breaking change documentation for merged PRs labeled + needs-breaking-change-doc-created. Produces two markdown files + (issue-draft.md and pr-comment.md) and optionally comments on the PR. + +concurrency: + group: "breaking-change-doc-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}" + cancel-in-progress: true + +permissions: + contents: read + pull-requests: read + issues: read + +tools: + bash: ["pwsh", "gh"] + +safe-outputs: + add-comment: + target: "*" + noop: + report-as-issue: false # Disable posting noop messages as issue comments + +if: | + github.event_name == 'workflow_dispatch' || + ( + !github.event.repository.fork && + github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'needs-breaking-change-doc-created') + ) + +post-steps: + - name: Upload breaking change drafts + if: always() + uses: actions/upload-artifact@v4 + with: + name: breaking-change-docs + path: artifacts/docs/breakingChanges/ + retention-days: 30 + if-no-files-found: ignore + +on: + pull_request_target: + types: [closed, labeled] + workflow_dispatch: + inputs: + pr_number: + description: "Pull Request Number" + required: true + type: string + suppress_output: + description: "Suppress workflow output (dry-run — only produce markdown workflow artifacts)" + required: false + type: boolean + default: false +# ############################################################### +# Override the COPILOT_GITHUB_TOKEN secret usage for the workflow +# with a randomly-selected token from a pool of secrets. +# +# As soon as organization-level billing is offered for Agentic +# Workflows, this stop-gap approach will be removed. +# +# See: /.github/actions/select-copilot-pat/README.md +# ############################################################### + + # Add the pre-activation step of selecting a random PAT from the supplied secrets + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + name: Checkout the select-copilot-pat action folder + with: + persist-credentials: false + sparse-checkout: .github/actions/select-copilot-pat + sparse-checkout-cone-mode: true + fetch-depth: 1 + + - id: select-copilot-pat + name: Select Copilot token from pool + uses: ./.github/actions/select-copilot-pat + env: + SECRET_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_9: ${{ secrets.COPILOT_PAT_9 }} + +# Add the pre-activation output of the randomly selected PAT +jobs: + pre-activation: + outputs: + copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} + +# Override the COPILOT_GITHUB_TOKEN expression used in the activation job +# Consume the PAT number from the pre-activation step and select the corresponding secret +engine: + id: copilot + env: + # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow + # If none of the `COPILOT_PAT_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} +--- + +# Breaking Change Documentation + +Create breaking change documentation for the pull request identified below. + +## PR to document + +- If triggered by a pull request event, the PR number is `${{ github.event.pull_request.number }}`. +- If triggered by `workflow_dispatch`, the PR number is `${{ github.event.inputs.pr_number }}`. + +## Dry-run mode + +- If triggered by `workflow_dispatch` with `suppress_output` = `true`, + **do not** post a comment on the PR after producing the files. Just + write the markdown files and stop. +- For pull_request triggers, always post the comment. + +## Instructions + +Using the breaking-change-doc skill from +`.github/skills/breaking-change-doc/SKILL.md`, execute **all steps (0 through +6)** for the PR above. + +In Step 6, if dry-run mode is active, skip publishing any output to the pull +request. The generated files in `artifacts/docs/breakingChanges/` are +automatically uploaded as a workflow artifact named **breaking-change-docs** +and can be downloaded from the workflow run summary page. + +## When no action is needed + +If no action is needed (PR has no area label, documentation already exists, +etc.), you MUST call the `noop` tool with a message explaining why: + +```json +{"noop": {"message": "No action needed: [brief explanation]"}} +``` diff --git a/.github/workflows/code-review.lock.yml b/.github/workflows/code-review.lock.yml index 1c0520d08d92f9..1bcee15bcd04d8 100644 --- a/.github/workflows/code-review.lock.yml +++ b/.github/workflows/code-review.lock.yml @@ -80,7 +80,7 @@ jobs: title: ${{ steps.sanitized.outputs.title }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.64.2 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Generate agentic run info @@ -300,7 +300,7 @@ jobs: output_types: ${{ steps.collect_output.outputs.output_types }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.64.2 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Set runtime paths @@ -917,7 +917,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.64.2 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact @@ -1013,7 +1013,7 @@ jobs: select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.64.2 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Check team membership for workflow @@ -1075,7 +1075,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.64.2 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact diff --git a/.github/workflows/copilot-echo.lock.yml b/.github/workflows/copilot-echo.lock.yml index ea7efe6f70fdef..8fee66da4b665f 100644 --- a/.github/workflows/copilot-echo.lock.yml +++ b/.github/workflows/copilot-echo.lock.yml @@ -81,7 +81,7 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.63.0 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Generate agentic run info @@ -290,7 +290,7 @@ jobs: output_types: ${{ steps.collect_output.outputs.output_types }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.63.0 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Set runtime paths @@ -904,7 +904,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.63.0 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact @@ -999,7 +999,7 @@ jobs: select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.63.0 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Check team membership for workflow @@ -1061,7 +1061,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.63.0 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact diff --git a/.github/workflows/labeler-cache-retention.yml b/.github/workflows/labeler-cache-retention.yml index 2c4013895af0f3..1289f756b077b9 100644 --- a/.github/workflows/labeler-cache-retention.yml +++ b/.github/workflows/labeler-cache-retention.yml @@ -33,7 +33,7 @@ jobs: matrix: type: ["issues", "pulls"] steps: - - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: ${{ matrix.type }} cache_key: ${{ env.CACHE_KEY }} diff --git a/.github/workflows/labeler-predict-issues.yml b/.github/workflows/labeler-predict-issues.yml index 89cc11af409508..c206eb03952616 100644 --- a/.github/workflows/labeler-predict-issues.yml +++ b/.github/workflows/labeler-predict-issues.yml @@ -40,7 +40,7 @@ jobs: steps: - name: "Restore issues model from cache" id: restore-model - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: issues fail-on-cache-miss: ${{ env.ALLOW_FAILURE }} @@ -49,7 +49,7 @@ jobs: - name: "Predict issue labels" id: prediction if: ${{ steps.restore-model.outputs.cache-hit == 'true' }} - uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: issues: ${{ inputs.issues || github.event.issue.number }} label_prefix: ${{ env.LABEL_PREFIX }} diff --git a/.github/workflows/labeler-predict-pulls.yml b/.github/workflows/labeler-predict-pulls.yml index dc35e65490a108..30652b9106ec15 100644 --- a/.github/workflows/labeler-predict-pulls.yml +++ b/.github/workflows/labeler-predict-pulls.yml @@ -114,7 +114,7 @@ jobs: - name: "Restore pulls model from cache" id: restore-model if: ${{ steps.determine-pulls.outputs.pulls != '' }} - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: pulls fail-on-cache-miss: ${{ env.ALLOW_FAILURE }} @@ -123,7 +123,7 @@ jobs: - name: "Predict pull labels" id: prediction if: ${{ steps.determine-pulls.outputs.pulls != '' && steps.restore-model.outputs.cache-hit == 'true' }} - uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: pulls: ${{ steps.determine-pulls.outputs.pulls }} label_prefix: ${{ env.LABEL_PREFIX }} diff --git a/.github/workflows/labeler-promote.yml b/.github/workflows/labeler-promote.yml index c01086c5177938..5f6770a75b4a0d 100644 --- a/.github/workflows/labeler-promote.yml +++ b/.github/workflows/labeler-promote.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Promote Model for Issues" - uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "issues" staged_key: ${{ inputs.staged_key }} @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Promote Model for Pull Requests" - uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "pulls" staged_key: ${{ inputs.staged_key }} diff --git a/.github/workflows/labeler-train.yml b/.github/workflows/labeler-train.yml index d94630c9f44478..51b802c048862c 100644 --- a/.github/workflows/labeler-train.yml +++ b/.github/workflows/labeler-train.yml @@ -61,7 +61,7 @@ jobs: issues: read steps: - name: "Download Issues" - uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "issues" cache_key: ${{ env.CACHE_KEY }} @@ -80,7 +80,7 @@ jobs: pull-requests: read steps: - name: "Download Pull Requests" - uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "pulls" cache_key: ${{ env.CACHE_KEY }} @@ -99,7 +99,7 @@ jobs: needs: download-issues steps: - name: "Train Model for Issues" - uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "issues" data_cache_key: ${{ env.CACHE_KEY }} @@ -112,7 +112,7 @@ jobs: needs: download-pulls steps: - name: "Train Model for Pull Requests" - uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "pulls" data_cache_key: ${{ env.CACHE_KEY }} @@ -126,7 +126,7 @@ jobs: needs: train-issues steps: - name: "Test Model for Issues" - uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "issues" cache_key: ${{ env.CACHE_KEY }} @@ -147,7 +147,7 @@ jobs: needs: train-pulls steps: - name: "Test Model for Pull Requests" - uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "pulls" cache_key: ${{ env.CACHE_KEY }} From 505abeb69f8db392a7cd476e1e61d0346eb08246 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:50:59 -0700 Subject: [PATCH 38/74] Revert more changes --- .../IL/ReadyToRunILProvider.cs | 4 +- .../CrossModuleInliningInfoSection.cs | 346 ------------------ .../InliningInfoSection2.cs | 194 +--------- .../MethodDefEntryPointsTable.cs | 150 -------- .../NativeSparseArray.cs | 114 ------ 5 files changed, 3 insertions(+), 805 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeSparseArray.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs index 8e7e1bbd939c3f..0f4adb3b43a016 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs @@ -155,9 +155,7 @@ public bool NeedsCrossModuleInlineableTokens(MethodDesc method) { bool regularCrossModuleInlineable = (!_compilationModuleGroup.VersionsWithMethodBody(method) && _compilationModuleGroup.CrossModuleInlineable(method)); - // bool requiredCrossModuleInliningForAsync = (NeedsTaskReturningThunk(method) || NeedsAsyncThunk(method) || method is AsyncResumptionStub) - // && !_compilationModuleGroup.VersionsWithModule(method.Context.SystemModule); - if ((regularCrossModuleInlineable) // || requiredCrossModuleInliningForAsync) + if ((regularCrossModuleInlineable) && !_manifestModuleWrappedMethods.ContainsKey(method)) { return true; diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs deleted file mode 100644 index 2a9cde6b501ec4..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs +++ /dev/null @@ -1,346 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection.Metadata; -using System.Reflection.Metadata.Ecma335; -using System.Text; -using Internal.ReadyToRunConstants; - -namespace ILCompiler.Reflection.ReadyToRun -{ - /// - /// Parser for the CrossModuleInlineInfo section (ReadyToRunSectionType 119, added in R2R v6.2). - /// This format differs from InliningInfo2 (section 114) — it uses a stream-size counted - /// encoding with 2-bit flags on the inlinee index and supports ILBody import indices for - /// cross-module inlinees and inliners. - /// - public class CrossModuleInliningInfoSection - { - public enum InlineeReferenceKind - { - Local, - CrossModule, - } - - private enum CrossModuleInlineFlags : uint - { - CrossModuleInlinee = 0x1, - HasCrossModuleInliners = 0x2, - CrossModuleInlinerIndexShift = 2, - InlinerRidHasModule = 0x1, - InlinerRidShift = 1, - } - - /// - /// Identifies a method in the inlining info section. - /// For cross-module methods, Index is an ILBody import section index. - /// For local methods, Index is a MethodDef RID and ModuleIndex identifies the owning module. - /// - public readonly struct MethodRef - { - public bool IsCrossModule { get; } - public uint Index { get; } - public uint ModuleIndex { get; } - - public MethodRef(bool isCrossModule, uint index, uint moduleIndex = 0) - { - IsCrossModule = isCrossModule; - Index = index; - ModuleIndex = moduleIndex; - } - } - - /// - /// A single inlinee and all the methods that inline it. - /// - public readonly struct InliningEntry - { - public MethodRef Inlinee { get; } - public IReadOnlyList Inliners { get; } - - public InliningEntry(MethodRef inlinee, IReadOnlyList inliners) - { - Inlinee = inlinee; - Inliners = inliners; - } - } - - private readonly ReadyToRunReader _r2r; - private readonly int _startOffset; - private readonly int _endOffset; - private readonly bool _multiModuleFormat; - - public CrossModuleInliningInfoSection(ReadyToRunReader reader, int offset, int endOffset) - { - _r2r = reader; - _startOffset = offset; - _endOffset = endOffset; - _multiModuleFormat = (reader.ReadyToRunHeader.Flags & (uint)ReadyToRunFlags.READYTORUN_FLAG_MultiModuleVersionBubble) != 0; - } - - /// - /// Parses the section into structured inlining entries. - /// - public List GetEntries() - { - var entries = new List(); - - NativeParser parser = new NativeParser(_r2r.ImageReader, (uint)_startOffset); - NativeHashtable hashtable = new NativeHashtable(_r2r.ImageReader, parser, (uint)_endOffset); - - var enumerator = hashtable.EnumerateAllEntries(); - NativeParser curParser = enumerator.GetNext(); - while (!curParser.IsNull()) - { - uint streamSize = curParser.GetUnsigned(); - uint inlineeIndexAndFlags = curParser.GetUnsigned(); - streamSize--; - - uint inlineeIndex = inlineeIndexAndFlags >> (int)CrossModuleInlineFlags.CrossModuleInlinerIndexShift; - bool hasCrossModuleInliners = (inlineeIndexAndFlags & (uint)CrossModuleInlineFlags.HasCrossModuleInliners) != 0; - bool crossModuleInlinee = (inlineeIndexAndFlags & (uint)CrossModuleInlineFlags.CrossModuleInlinee) != 0; - - MethodRef inlinee; - if (crossModuleInlinee) - { - inlinee = new MethodRef(isCrossModule: true, index: inlineeIndex); - } - else - { - uint moduleIndex = 0; - if (_multiModuleFormat && streamSize > 0) - { - moduleIndex = curParser.GetUnsigned(); - streamSize--; - } - inlinee = new MethodRef(isCrossModule: false, index: inlineeIndex, moduleIndex: moduleIndex); - } - - var inliners = new List(); - - if (hasCrossModuleInliners && streamSize > 0) - { - uint crossModuleInlinerCount = curParser.GetUnsigned(); - streamSize--; - - // Cross-module inliner indices are absolute ILBody import indices, - // not delta-encoded (the writer never updates baseIndex between entries). - for (uint i = 0; i < crossModuleInlinerCount && streamSize > 0; i++) - { - uint inlinerIndex = curParser.GetUnsigned(); - streamSize--; - inliners.Add(new MethodRef(isCrossModule: true, index: inlinerIndex)); - } - } - - uint currentRid = 0; - while (streamSize > 0) - { - uint inlinerDeltaAndFlag = curParser.GetUnsigned(); - streamSize--; - - uint moduleIndex = 0; - if (_multiModuleFormat) - { - currentRid += inlinerDeltaAndFlag >> (int)CrossModuleInlineFlags.InlinerRidShift; - if ((inlinerDeltaAndFlag & (uint)CrossModuleInlineFlags.InlinerRidHasModule) != 0 && streamSize > 0) - { - moduleIndex = curParser.GetUnsigned(); - streamSize--; - } - } - else - { - currentRid += inlinerDeltaAndFlag; - } - inliners.Add(new MethodRef(isCrossModule: false, index: currentRid, moduleIndex: moduleIndex)); - } - - entries.Add(new InliningEntry(inlinee, inliners)); - curParser = enumerator.GetNext(); - } - - return entries; - } - - /// - /// Resolves a to a human-readable method name. - /// Cross-module methods are resolved via the ILBody import section entries. - /// Local methods are resolved via the R2R reader's compiled method table. - /// - public string ResolveMethodName(MethodRef methodRef) - { - if (methodRef.IsCrossModule) - { - return ResolveCrossModuleMethod(methodRef.Index); - } - - return ResolveLocalMethod(methodRef.Index); - } - - /// - /// Returns all inlining pairs with resolved method names and whether the inlinee is cross-module. - /// - public IEnumerable<(string InlinerName, string InlineeName, InlineeReferenceKind InlineeKind)> GetInliningPairs() - { - foreach (var entry in GetEntries()) - { - string inlineeName = ResolveMethodName(entry.Inlinee); - var kind = entry.Inlinee.IsCrossModule ? InlineeReferenceKind.CrossModule : InlineeReferenceKind.Local; - foreach (var inliner in entry.Inliners) - { - yield return (ResolveMethodName(inliner), inlineeName, kind); - } - } - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - foreach (var entry in GetEntries()) - { - if (entry.Inlinee.IsCrossModule) - { - sb.AppendLine($"Inliners for cross-module inlinee (ILBody import index {entry.Inlinee.Index}):"); - } - else - { - int token = RidToMethodDef((int)entry.Inlinee.Index); - if (_multiModuleFormat) - { - string moduleName = TryGetModuleName(entry.Inlinee.ModuleIndex); - sb.AppendLine($"Inliners for inlinee {token:X8} (module {moduleName}):"); - } - else - { - sb.AppendLine($"Inliners for inlinee {token:X8}:"); - } - } - - foreach (var inliner in entry.Inliners) - { - if (inliner.IsCrossModule) - { - sb.AppendLine($" cross-module inliner (ILBody import index {inliner.Index})"); - } - else - { - int token = RidToMethodDef((int)inliner.Index); - if (inliner.ModuleIndex != 0 || _multiModuleFormat) - { - string moduleName = TryGetModuleName(inliner.ModuleIndex); - sb.AppendLine($" {token:X8} (module {moduleName})"); - } - else - { - sb.AppendLine($" {token:X8}"); - } - } - } - } - - return sb.ToString(); - } - - private string ResolveCrossModuleMethod(uint importIndex) - { - if (!_ilBodyImportSectionResolved) - { - _ilBodyImportSection = FindILBodyImportSection(); - _ilBodyImportSectionResolved = true; - } - - if (_ilBodyImportSection.Entries is not null && importIndex < (uint)_ilBodyImportSection.Entries.Count) - { - var entry = _ilBodyImportSection.Entries[(int)importIndex]; - if (entry.Signature is not null) - { - string sig = entry.Signature.ToString(new SignatureFormattingOptions()); - int parenIdx = sig.LastIndexOf(" (", StringComparison.Ordinal); - - return parenIdx >= 0 ? sig[..parenIdx] : sig; - } - } - - return $""; - } - - private string ResolveLocalMethod(uint rid) - { - _localMethodMap ??= BuildLocalMethodMap(); - string name; - - return _localMethodMap.TryGetValue(rid, out name) - ? name - : $""; - } - - private ReadyToRunImportSection FindILBodyImportSection() - { - foreach (var section in _r2r.ImportSections) - { - foreach (var entry in section.Entries) - { - if (entry.Signature?.FixupKind is ReadyToRunFixupKind.Check_IL_Body or ReadyToRunFixupKind.Verify_IL_Body) - return section; - } - } - - return default; - } - - private Dictionary BuildLocalMethodMap() - { - var map = new Dictionary(); - foreach (var assembly in _r2r.ReadyToRunAssemblies) - { - foreach (var method in assembly.Methods) - { - if (method.MethodHandle.Kind == HandleKind.MethodDefinition) - { - uint methodRid = (uint)MetadataTokens.GetRowNumber((MethodDefinitionHandle)method.MethodHandle); - map[methodRid] = method.SignatureString; - } - } - } - - foreach (var instanceEntry in _r2r.InstanceMethods) - { - if (instanceEntry.Method.MethodHandle.Kind == HandleKind.MethodDefinition) - { - uint methodRid = (uint)MetadataTokens.GetRowNumber((MethodDefinitionHandle)instanceEntry.Method.MethodHandle); - map.TryAdd(methodRid, instanceEntry.Method.SignatureString); - } - } - - return map; - } - - private ReadyToRunImportSection _ilBodyImportSection; - private bool _ilBodyImportSectionResolved; - private Dictionary _localMethodMap; - - private string TryGetModuleName(uint moduleIndex) - { - if (moduleIndex == 0 && !_r2r.Composite) - { - return Path.GetFileNameWithoutExtension(_r2r.Filename); - } - - try - { - return _r2r.GetReferenceAssemblyName((int)moduleIndex); - } - catch - { - return $""; - } - } - - static int RidToMethodDef(int rid) => MetadataTokens.GetToken(MetadataTokens.MethodDefinitionHandle(rid)); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/InliningInfoSection2.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/InliningInfoSection2.cs index cf48b4f077f3cb..036c771e1bb215 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/InliningInfoSection2.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/InliningInfoSection2.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; -using System.IO; -using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Text; @@ -22,189 +19,6 @@ public InliningInfoSection2(ReadyToRunReader reader, int offset, int endOffset) _endOffset = endOffset; } - /// - /// A raw inlining entry: one inlinee with its list of inliners. - /// RIDs are MethodDef row numbers. Module index identifies which component - /// assembly in a composite image the method belongs to (0 = owner module). - /// - public readonly struct InliningEntry - { - public int InlineeRid { get; } - public uint InlineeModuleIndex { get; } - public bool InlineeHasModule { get; } - public IReadOnlyList<(int Rid, uint ModuleIndex, bool HasModule)> Inliners { get; } - - public InliningEntry(int inlineeRid, uint inlineeModuleIndex, bool inlineeHasModule, - IReadOnlyList<(int, uint, bool)> inliners) - { - InlineeRid = inlineeRid; - InlineeModuleIndex = inlineeModuleIndex; - InlineeHasModule = inlineeHasModule; - Inliners = inliners; - } - } - - /// - /// Parses all entries from the InliningInfo2 section. - /// - public List GetEntries() - { - var entries = new List(); - - NativeParser parser = new NativeParser(_r2r.ImageReader, (uint)_startOffset); - NativeHashtable hashtable = new NativeHashtable(_r2r.ImageReader, parser, (uint)_endOffset); - - var enumerator = hashtable.EnumerateAllEntries(); - - NativeParser curParser = enumerator.GetNext(); - while (!curParser.IsNull()) - { - int count = (int)curParser.GetUnsigned(); - int inlineeRidAndFlag = (int)curParser.GetUnsigned(); - count--; - int inlineeRid = inlineeRidAndFlag >> 1; - - uint inlineeModule = 0; - bool inlineeHasModule = (inlineeRidAndFlag & 1) != 0; - if (inlineeHasModule) - { - inlineeModule = curParser.GetUnsigned(); - count--; - } - - var inliners = new List<(int, uint, bool)>(); - int currentRid = 0; - while (count > 0) - { - int inlinerDeltaAndFlag = (int)curParser.GetUnsigned(); - count--; - int inlinerDelta = inlinerDeltaAndFlag >> 1; - currentRid += inlinerDelta; - - uint inlinerModule = 0; - bool inlinerHasModule = (inlinerDeltaAndFlag & 1) != 0; - if (inlinerHasModule) - { - inlinerModule = curParser.GetUnsigned(); - count--; - } - - inliners.Add((currentRid, inlinerModule, inlinerHasModule)); - } - - entries.Add(new InliningEntry(inlineeRid, inlineeModule, inlineeHasModule, inliners)); - curParser = enumerator.GetNext(); - } - - return entries; - } - - /// - /// Returns all inlining pairs with resolved method names. - /// - public IEnumerable<(string InlinerName, string InlineeName)> GetInliningPairs() - { - _localMethodMap ??= BuildLocalMethodMap(); - - foreach (var entry in GetEntries()) - { - string inlineeName = ResolveMethod(entry.InlineeRid, entry.InlineeModuleIndex, entry.InlineeHasModule); - foreach (var (rid, moduleIndex, hasModule) in entry.Inliners) - { - string inlinerName = ResolveMethod(rid, moduleIndex, hasModule); - yield return (inlinerName, inlineeName); - } - } - } - - private string ResolveMethod(int rid, uint moduleIndex, bool hasModule) - { - if (hasModule) - { - string moduleName = TryGetModuleName(moduleIndex); - return $"{moduleName}!{ResolveMethodInModule(rid, moduleIndex)}"; - } - - if (_localMethodMap.TryGetValue((uint)rid, out string name)) - return name; - - return $""; - } - - private string ResolveMethodInModule(int rid, uint moduleIndex) - { - try - { - IAssemblyMetadata asmMeta = _r2r.OpenReferenceAssembly((int)moduleIndex); - if (asmMeta is not null) - { - var mdReader = asmMeta.MetadataReader; - var handle = MetadataTokens.MethodDefinitionHandle(rid); - if (mdReader.GetTableRowCount(TableIndex.MethodDef) >= rid) - { - var methodDef = mdReader.GetMethodDefinition(handle); - string typeName = ""; - if (!methodDef.GetDeclaringType().IsNil) - { - var typeDef = mdReader.GetTypeDefinition(methodDef.GetDeclaringType()); - typeName = mdReader.GetString(typeDef.Name) + "."; - } - return typeName + mdReader.GetString(methodDef.Name); - } - } - } - catch - { - // Fall through to token-based name - } - - return $""; - } - - private string TryGetModuleName(uint moduleIndex) - { - if (moduleIndex == 0 && !_r2r.Composite) - return Path.GetFileNameWithoutExtension(_r2r.Filename); - - try - { - return _r2r.GetReferenceAssemblyName((int)moduleIndex); - } - catch - { - return $""; - } - } - - private Dictionary BuildLocalMethodMap() - { - var map = new Dictionary(); - foreach (var assembly in _r2r.ReadyToRunAssemblies) - { - foreach (var method in assembly.Methods) - { - if (method.MethodHandle.Kind == HandleKind.MethodDefinition) - { - uint methodRid = (uint)MetadataTokens.GetRowNumber((MethodDefinitionHandle)method.MethodHandle); - map[methodRid] = method.SignatureString; - } - } - } - - foreach (var instanceEntry in _r2r.InstanceMethods) - { - if (instanceEntry.Method.MethodHandle.Kind == HandleKind.MethodDefinition) - { - uint methodRid = (uint)MetadataTokens.GetRowNumber((MethodDefinitionHandle)instanceEntry.Method.MethodHandle); - map.TryAdd(methodRid, instanceEntry.Method.SignatureString); - } - } - - return map; - } - - private Dictionary _localMethodMap; - public override string ToString() { StringBuilder sb = new StringBuilder(); @@ -225,9 +39,7 @@ public override string ToString() { uint module = curParser.GetUnsigned(); count--; - string moduleName = (int)module < _r2r.ManifestReferences.Count + 1 - ? _r2r.GetReferenceAssemblyName((int)module) - : $""; + string moduleName = _r2r.GetReferenceAssemblyName((int)module); sb.AppendLine($"Inliners for inlinee {inlineeToken:X8} (module {moduleName}):"); } else @@ -248,9 +60,7 @@ public override string ToString() { uint module = curParser.GetUnsigned(); count--; - string moduleName = (int)module < _r2r.ManifestReferences.Count + 1 - ? _r2r.GetReferenceAssemblyName((int)module) - : $""; + string moduleName = _r2r.GetReferenceAssemblyName((int)module); sb.AppendLine($" {inlinerToken:X8} (module {moduleName})"); } else diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs deleted file mode 100644 index fc5c2209dadb56..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/MethodDefEntryPointsTable.cs +++ /dev/null @@ -1,150 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; - -namespace ILCompiler.Reflection.ReadyToRun -{ - /// - /// Structural projection of the MethodDefEntryPoints NativeArray section. - /// Each entry maps a MethodDef RID to its RuntimeFunction index and the - /// fixup cell references (import section table + cell index pairs) it needs - /// resolved before execution. - /// - public sealed class MethodDefEntryPointsTable - { - public IReadOnlyList Entries { get; } - - private MethodDefEntryPointsTable(List entries) - { - Entries = entries; - } - - /// - /// Parse a MethodDefEntryPoints section from the R2R image. - /// - /// The R2R reader containing the image. - /// The MethodDefEntryPoints section to parse. - public static MethodDefEntryPointsTable Parse(ReadyToRunReader reader, ReadyToRunSection section) - { - int sectionOffset = reader.GetOffset(section.RelativeVirtualAddress); - NativeArray methodEntryPoints = new NativeArray(reader.ImageReader, (uint)sectionOffset); - uint count = methodEntryPoints.GetCount(); - - var entries = new List((int)count); - - for (uint rid = 1; rid <= count; rid++) - { - int offset = 0; - if (!methodEntryPoints.TryGetAt(rid - 1, ref offset)) - { - continue; - } - - // Decode the entry point: compressed uint with RuntimeFunction index + fixup flag - uint id = 0; - offset = (int)reader.ImageReader.DecodeUnsigned((uint)offset, ref id); - - int? fixupOffset = null; - uint runtimeFunctionIndex; - - if ((id & 1) != 0) - { - // Has fixups - if ((id & 2) != 0) - { - // Fixup list uses a backward offset - uint val = 0; - reader.ImageReader.DecodeUnsigned((uint)offset, ref val); - offset -= (int)val; - } - - fixupOffset = offset; - runtimeFunctionIndex = id >> 2; - } - else - { - runtimeFunctionIndex = id >> 1; - } - - // Parse the fixup delay list (nibble-encoded import section/slot pairs) - var fixupCells = new List(); - if (fixupOffset.HasValue) - { - NibbleReader nibbleReader = new NibbleReader(reader.ImageReader, fixupOffset.Value); - uint curTableIndex = nibbleReader.ReadUInt(); - - while (true) - { - uint cellIndex = nibbleReader.ReadUInt(); - - while (true) - { - fixupCells.Add(new FixupCellRef(curTableIndex, cellIndex)); - - uint delta = nibbleReader.ReadUInt(); - if (delta == 0) - break; - - cellIndex += delta; - } - - uint tableDelta = nibbleReader.ReadUInt(); - if (tableDelta == 0) - break; - - curTableIndex += tableDelta; - } - } - - entries.Add(new MethodDefEntry(rid, runtimeFunctionIndex, fixupCells)); - } - - return new MethodDefEntryPointsTable(entries); - } - } - - /// - /// One entry in the MethodDefEntryPoints table: a compiled method identified by - /// its MethodDef RID, with its RuntimeFunction index and fixup cell references. - /// Null entries in indicate - /// MethodDef RIDs that have no compiled entrypoint. - /// - public sealed class MethodDefEntry - { - /// MethodDef RID (1-based). - public uint Rid { get; } - - /// Index into the RuntimeFunctions array. - public uint EntryPointIndex { get; } - - /// Fixup cells this method needs resolved before execution. - public IReadOnlyList FixupCells { get; } - - public MethodDefEntry(uint rid, uint entryPointIndex, List fixupCells) - { - Rid = rid; - EntryPointIndex = entryPointIndex; - FixupCells = fixupCells; - } - } - - /// - /// A reference to a single fixup cell: identifies the import section and - /// entry index within that section. - /// - public sealed class FixupCellRef - { - /// Index of the import section in the ImportSections array. - public uint TableIndex { get; } - - /// Index of the entry within the import section. - public uint CellIndex { get; } - - public FixupCellRef(uint tableIndex, uint cellIndex) - { - TableIndex = tableIndex; - CellIndex = cellIndex; - } - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeSparseArray.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeSparseArray.cs deleted file mode 100644 index 65a86510337f91..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeSparseArray.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.IO; -using System.Text; - -namespace ILCompiler.Reflection.ReadyToRun -{ - /// - /// based on NativeFormat::NativeArray - /// - public class NativeSparseArray - { - private const int _blockSize = 16; - - private NativeReader _reader; - private uint _baseOffset; - private uint _nElements; - private byte _entryIndexSize; - - public NativeSparseArray(NativeReader reader, uint offset) - { - _reader = reader; - - uint val = 0; - _baseOffset = _reader.DecodeUnsigned(offset, ref val); - _nElements = (val >> 2); - _entryIndexSize = (byte)(val & 3); - } - - public uint GetCount() - { - return _nElements; - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - - sb.AppendLine($"NativeArray Size: {_nElements}"); - sb.AppendLine($"EntryIndexSize: {_entryIndexSize}"); - for (uint i = 0; i < _nElements; i++) - { - int val = 0; - if (TryGetAt(i, ref val)) - { - sb.AppendLine($"{i}: {val}"); - } - } - - return sb.ToString(); - } - - public bool TryGetAt(uint index, ref int pOffset) - { - if (index >= _nElements) - return false; - - uint offset; - if (_entryIndexSize == 0) - { - int i = (int)(_baseOffset + (index / _blockSize)); - offset = _reader.ReadByte(ref i); - } - else if (_entryIndexSize == 1) - { - int i = (int)(_baseOffset + 2 * (index / _blockSize)); - offset = _reader.ReadUInt16(ref i); - } - else - { - int i = (int)(_baseOffset + 4 * (index / _blockSize)); - offset = _reader.ReadUInt32(ref i); - } - offset += _baseOffset; - - for (uint bit = _blockSize >> 1; bit > 0; bit >>= 1) - { - uint val = 0; - uint offset2 = _reader.DecodeUnsigned(offset, ref val); - if ((index & bit) != 0) - { - if ((val & 2) != 0) - { - offset += val >> 2; - continue; - } - } - else - { - if ((val & 1) != 0) - { - offset = offset2; - continue; - } - } - - // Not found - if ((val & 3) == 0) - { - // Matching special leaf node? - if ((val >> 2) == (index & (_blockSize - 1))) - { - offset = offset2; - break; - } - } - return false; - } - pOffset = (int)offset; - return true; - } - } -} From 8edd9b9139f31c758207c546a4123d45cac9e371 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:54:26 -0700 Subject: [PATCH 39/74] Revert changes to .github to the right version? --- .github/actions/select-copilot-pat/README.md | 7 --- .github/aw/actions-lock.json | 15 ------- .github/dependabot.yml | 2 - .github/policies/resourceManagement.yml | 16 ------- .github/skills/api-proposal/SKILL.md | 44 +------------------ .../references/api-proposal-checklist.md | 11 ----- .github/workflows/code-review.lock.yml | 10 ++--- .github/workflows/copilot-echo.lock.yml | 10 ++--- .github/workflows/labeler-cache-retention.yml | 2 +- .github/workflows/labeler-predict-issues.yml | 4 +- .github/workflows/labeler-predict-pulls.yml | 4 +- .github/workflows/labeler-promote.yml | 4 +- .github/workflows/labeler-train.yml | 12 ++--- .github/workflows/labeler.md | 40 +++++++++++++++++ 14 files changed, 64 insertions(+), 117 deletions(-) create mode 100644 .github/workflows/labeler.md diff --git a/.github/actions/select-copilot-pat/README.md b/.github/actions/select-copilot-pat/README.md index 8b57f6377bcc84..12eeafe5b1f817 100644 --- a/.github/actions/select-copilot-pat/README.md +++ b/.github/actions/select-copilot-pat/README.md @@ -10,11 +10,6 @@ To use Agentic Workflows in a dotnet org repository: 2. Copy this `select-copilot-pat` folder into the repository under `.github/actions/select-copilot-pat`, including both the `README.md` and `action.yml`. 3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below. -> **Optional:** If you plan to manage secrets or workflows from the command line (e.g., `gh aw secrets set`), [install the `gh aw` CLI extension][cli-setup]: -> ```sh -> gh extension install github/gh-aw -> ``` - ## PAT Management Team members provide PATs into the pools for the repository by adding them as repository secrets with secret names matching the pattern of `_<0-9>`, such as `COPILOT_PAT_0`. @@ -90,7 +85,6 @@ engine: ## References -- [Agentic Workflows CLI Extension][cli-setup] - [Agentic Authoring][configure-repo] - [Authentication][authentication] - [Agentic Workflow Imports][imports] @@ -102,7 +96,6 @@ engine: - [Case Function in Workflow Expressions][case-expression] - [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override] -[cli-setup]: https://github.github.com/gh-aw/setup/cli/ [configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository [authentication]: https://github.github.com/gh-aw/reference/auth/ [create-pat]: https://github.com/settings/personal-access-tokens/new?name=dotnet%20org%20agentic%20workflows&description=GitHub+Agentic+Workflows+-+Copilot+engine+authentication.++Used+for+dotnet+org+workflows.+MUST+be+configured+with+only+Copilot+Requests+permissions+and+user+account+as+resource+owner.+Weekly+expiration+and+required+renewal.&user_copilot_requests=read&expires_in=8 diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 4ff4a7ea77497e..959efc4f8604ed 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -5,11 +5,6 @@ "version": "v8", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" }, - "actions/upload-artifact@v4": { - "repo": "actions/upload-artifact", - "version": "v4", - "sha": "ea165f8d65b6e75b540449e92b4886f43607fa02" - }, "github/gh-aw-actions/setup@v0.63.0": { "repo": "github/gh-aw-actions/setup", "version": "v0.63.0", @@ -19,16 +14,6 @@ "repo": "github/gh-aw-actions/setup", "version": "v0.63.1", "sha": "53e09ec0be6271e81a69f51ef93f37212c8834b0" - }, - "github/gh-aw-actions/setup@v0.64.5": { - "repo": "github/gh-aw-actions/setup", - "version": "v0.64.5", - "sha": "5d2ebfd87a1a45a8a8323c1a12c01b055730dac5" - }, - "github/gh-aw-actions/setup@v0.65.6": { - "repo": "github/gh-aw-actions/setup", - "version": "v0.65.6", - "sha": "31130b20a8fd3ef263acbe2091267c0aace07e09" } } } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e7c47261b8ebab..806f7fad67b045 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,8 +7,6 @@ updates: open-pull-requests-limit: 5 labels: - area-codeflow - exclude-paths: - - ".github/workflows/*.lock.yml" ignore: - dependency-name: "actions/checkout" update-types: ["version-update:semver-patch","version-update:semver-minor"] diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml index 05f03bb7612a09..f192c82b6da5e5 100644 --- a/.github/policies/resourceManagement.yml +++ b/.github/policies/resourceManagement.yml @@ -146,8 +146,6 @@ configuration: label: area-Debugger-mono - labelAdded: label: area-DependencyModel - - labelAdded: - label: area-Diagnostics-cdac - labelAdded: label: area-Diagnostics-coreclr - labelAdded: @@ -492,20 +490,6 @@ configuration: See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed. assignMentionees: False - if: - - hasLabel: - label: area-Diagnostics-cdac - then: - - mentionUsers: - mentionees: - - steveisok - - tommcdon - - dotnet/dotnet-diag - replyTemplate: >- - Tagging subscribers to this area: ${mentionees} - - See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed. - assignMentionees: False - - if: - hasLabel: label: area-Diagnostics-coreclr then: diff --git a/.github/skills/api-proposal/SKILL.md b/.github/skills/api-proposal/SKILL.md index 80d1ca0d9d070e..8f1905b87d1428 100644 --- a/.github/skills/api-proposal/SKILL.md +++ b/.github/skills/api-proposal/SKILL.md @@ -113,23 +113,7 @@ The skill contains baked-in examples and guidelines for writing good proposals ( - Cover edge cases, null inputs, boundary conditions - Test any interaction with existing APIs -4. **Search dotnet/runtime for adoption sites.** Before writing any proposal text, systematically search the entire local repo (grep/ripgrep) for every place the new API could or should be used. This serves two purposes: validating that the API shape works in diverse real-world contexts, and producing the adoption catalog required in the proposal (see Phase 4, section 6). - - **What to search for:** - - Manual workarounds, verbose boilerplate, or older idioms that the new API directly replaces - - Call sites of the existing APIs being extended (e.g., if adding an overload, find every call to the current overloads) - - Patterns that would become simpler, more efficient, or more correct with the new API - - Third-party or test code within the repo that exercises the same scenario - - **How to catalog results:** - - Record every hit with file path and a one-line description of how the new API applies - - Classify each site into one of three categories: - - **Updated**: Already converted in the prototype commit (pick a representative set of the most diverse and impactful sites) - - **Candidate**: Could be converted but was intentionally deferred (e.g., different area, risk of churn, needs area-owner review) - - **Inapplicable**: Initially matched the search but doesn't actually benefit from the new API (brief reason why) - - The catalog should be thorough — err on the side of listing too many sites rather than too few - -5. **Apply the new API at representative sites.** From the catalog above, convert a diverse, representative set of adoption sites in the prototype commit. Prioritize sites that exercise different aspects of the API (different overloads, edge cases, interaction with other APIs). The remaining "Candidate" sites are listed in the proposal but left unconverted. +4. **Apply the new API throughout the runtime codebase** where relevant. Search (grep/ripgrep) for existing patterns that would benefit from the new API — e.g., manual workarounds, verbose boilerplate, or older idioms that the new API replaces. Include these call-site updates in the prototype commit to demonstrate real-world applicability and exercise the API in diverse contexts. #### Prototype Validation (all steps required) @@ -278,32 +262,6 @@ The agent has the burden of proof when claiming absence of risks. Evaluate: Write "No response" if there are genuinely no risks, matching the convention used in real `api-approved` issues. Do not inflate this section for straightforward additions. -**6. Usage in dotnet/runtime** - -Include the full adoption catalog produced during Phase 2, step 4. This section demonstrates that the API is broadly useful across the runtime codebase and helps area owners discover conversion opportunities in their code. - -Format as a table or grouped list: - -```markdown -#### Updated in prototype - -| File | Description | -|------|-------------| -| `src/libraries/System.Linq/src/System/Linq/Where.cs` | Replaced manual null-check + throw with `ArgumentNullException.ThrowIfNull` | -| `src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Queue.cs` | Replaced bounds-check boilerplate | - -#### Candidates for follow-up - -| File | Description | Why deferred | -|------|-------------|--------------| -| `src/coreclr/nativeaot/...` | Same pattern as above | Different area owner | -| `src/mono/...` | Uses equivalent Mono-specific helper | Needs area-owner review | -``` - -- Every "Updated" and "Candidate" site from the Phase 2 catalog must appear here. -- "Inapplicable" sites may be omitted from the proposal unless they illustrate an interesting design boundary (e.g., "this pattern looks like a match but isn't because X" — useful context for reviewers). -- If no adoption sites were found beyond the target library itself, state that explicitly — it's a signal reviewers will want to see. - #### After Drafting Present the complete draft to the user for review. Iterate based on feedback before publishing. diff --git a/.github/skills/api-proposal/references/api-proposal-checklist.md b/.github/skills/api-proposal/references/api-proposal-checklist.md index e4612571580743..88171bed7ed51e 100644 --- a/.github/skills/api-proposal/references/api-proposal-checklist.md +++ b/.github/skills/api-proposal/references/api-proposal-checklist.md @@ -31,17 +31,6 @@ Use this checklist to validate an API proposal before publishing. Items are orde - [ ] **DO** consider overload consistency with existing method families - [ ] **DO NOT** propose a narrow addition without evaluating the broader scope — reviewers will ask about it -## Adoption Catalog (Usage in dotnet/runtime) - -- [ ] **DO** search the entire dotnet/runtime repo for every place the proposed API could or should be used -- [ ] **DO** search for manual workarounds, verbose boilerplate, and older idioms the new API replaces -- [ ] **DO** search for call sites of existing APIs being extended (e.g., all callers of the current overloads) -- [ ] **DO** classify each site as Updated, Candidate, or Inapplicable -- [ ] **DO** convert a diverse, representative subset of sites in the prototype to exercise the API in real contexts -- [ ] **DO** include the full catalog in the proposal (Phase 4, section 6 — "Usage in dotnet/runtime") -- [ ] **DO** explicitly state if no adoption sites were found outside the target library — this is a signal reviewers need -- [ ] **DO NOT** limit the search to the target library alone — search `src/libraries/`, `src/coreclr/`, `src/mono/`, and other areas - ## Prototype Validation - [ ] **DO** verify the prototype builds for all target frameworks in the library's `.csproj` diff --git a/.github/workflows/code-review.lock.yml b/.github/workflows/code-review.lock.yml index 1bcee15bcd04d8..9143fc2cb8717f 100644 --- a/.github/workflows/code-review.lock.yml +++ b/.github/workflows/code-review.lock.yml @@ -80,7 +80,7 @@ jobs: title: ${{ steps.sanitized.outputs.title }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 + uses: github/gh-aw-actions/setup@53e09ec0be6271e81a69f51ef93f37212c8834b0 # v0.63.1 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Generate agentic run info @@ -300,7 +300,7 @@ jobs: output_types: ${{ steps.collect_output.outputs.output_types }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 + uses: github/gh-aw-actions/setup@53e09ec0be6271e81a69f51ef93f37212c8834b0 # v0.63.1 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Set runtime paths @@ -917,7 +917,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 + uses: github/gh-aw-actions/setup@53e09ec0be6271e81a69f51ef93f37212c8834b0 # v0.63.1 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact @@ -1013,7 +1013,7 @@ jobs: select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 + uses: github/gh-aw-actions/setup@53e09ec0be6271e81a69f51ef93f37212c8834b0 # v0.63.1 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Check team membership for workflow @@ -1075,7 +1075,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 + uses: github/gh-aw-actions/setup@53e09ec0be6271e81a69f51ef93f37212c8834b0 # v0.63.1 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact diff --git a/.github/workflows/copilot-echo.lock.yml b/.github/workflows/copilot-echo.lock.yml index 8fee66da4b665f..f4d97e2e509432 100644 --- a/.github/workflows/copilot-echo.lock.yml +++ b/.github/workflows/copilot-echo.lock.yml @@ -81,7 +81,7 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 + uses: github/gh-aw-actions/setup@9128d2542bbf1bdfec94dabeaf3e1d3c0d402577 # v0.63.0 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Generate agentic run info @@ -290,7 +290,7 @@ jobs: output_types: ${{ steps.collect_output.outputs.output_types }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 + uses: github/gh-aw-actions/setup@9128d2542bbf1bdfec94dabeaf3e1d3c0d402577 # v0.63.0 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Set runtime paths @@ -904,7 +904,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 + uses: github/gh-aw-actions/setup@9128d2542bbf1bdfec94dabeaf3e1d3c0d402577 # v0.63.0 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact @@ -999,7 +999,7 @@ jobs: select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 + uses: github/gh-aw-actions/setup@9128d2542bbf1bdfec94dabeaf3e1d3c0d402577 # v0.63.0 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Check team membership for workflow @@ -1061,7 +1061,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 + uses: github/gh-aw-actions/setup@9128d2542bbf1bdfec94dabeaf3e1d3c0d402577 # v0.63.0 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact diff --git a/.github/workflows/labeler-cache-retention.yml b/.github/workflows/labeler-cache-retention.yml index 1289f756b077b9..2c4013895af0f3 100644 --- a/.github/workflows/labeler-cache-retention.yml +++ b/.github/workflows/labeler-cache-retention.yml @@ -33,7 +33,7 @@ jobs: matrix: type: ["issues", "pulls"] steps: - - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: ${{ matrix.type }} cache_key: ${{ env.CACHE_KEY }} diff --git a/.github/workflows/labeler-predict-issues.yml b/.github/workflows/labeler-predict-issues.yml index c206eb03952616..89cc11af409508 100644 --- a/.github/workflows/labeler-predict-issues.yml +++ b/.github/workflows/labeler-predict-issues.yml @@ -40,7 +40,7 @@ jobs: steps: - name: "Restore issues model from cache" id: restore-model - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: issues fail-on-cache-miss: ${{ env.ALLOW_FAILURE }} @@ -49,7 +49,7 @@ jobs: - name: "Predict issue labels" id: prediction if: ${{ steps.restore-model.outputs.cache-hit == 'true' }} - uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: issues: ${{ inputs.issues || github.event.issue.number }} label_prefix: ${{ env.LABEL_PREFIX }} diff --git a/.github/workflows/labeler-predict-pulls.yml b/.github/workflows/labeler-predict-pulls.yml index 30652b9106ec15..dc35e65490a108 100644 --- a/.github/workflows/labeler-predict-pulls.yml +++ b/.github/workflows/labeler-predict-pulls.yml @@ -114,7 +114,7 @@ jobs: - name: "Restore pulls model from cache" id: restore-model if: ${{ steps.determine-pulls.outputs.pulls != '' }} - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: pulls fail-on-cache-miss: ${{ env.ALLOW_FAILURE }} @@ -123,7 +123,7 @@ jobs: - name: "Predict pull labels" id: prediction if: ${{ steps.determine-pulls.outputs.pulls != '' && steps.restore-model.outputs.cache-hit == 'true' }} - uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: pulls: ${{ steps.determine-pulls.outputs.pulls }} label_prefix: ${{ env.LABEL_PREFIX }} diff --git a/.github/workflows/labeler-promote.yml b/.github/workflows/labeler-promote.yml index 5f6770a75b4a0d..c01086c5177938 100644 --- a/.github/workflows/labeler-promote.yml +++ b/.github/workflows/labeler-promote.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Promote Model for Issues" - uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: "issues" staged_key: ${{ inputs.staged_key }} @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Promote Model for Pull Requests" - uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: "pulls" staged_key: ${{ inputs.staged_key }} diff --git a/.github/workflows/labeler-train.yml b/.github/workflows/labeler-train.yml index 51b802c048862c..d94630c9f44478 100644 --- a/.github/workflows/labeler-train.yml +++ b/.github/workflows/labeler-train.yml @@ -61,7 +61,7 @@ jobs: issues: read steps: - name: "Download Issues" - uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: "issues" cache_key: ${{ env.CACHE_KEY }} @@ -80,7 +80,7 @@ jobs: pull-requests: read steps: - name: "Download Pull Requests" - uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: "pulls" cache_key: ${{ env.CACHE_KEY }} @@ -99,7 +99,7 @@ jobs: needs: download-issues steps: - name: "Train Model for Issues" - uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: "issues" data_cache_key: ${{ env.CACHE_KEY }} @@ -112,7 +112,7 @@ jobs: needs: download-pulls steps: - name: "Train Model for Pull Requests" - uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: "pulls" data_cache_key: ${{ env.CACHE_KEY }} @@ -126,7 +126,7 @@ jobs: needs: train-issues steps: - name: "Test Model for Issues" - uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: "issues" cache_key: ${{ env.CACHE_KEY }} @@ -147,7 +147,7 @@ jobs: needs: train-pulls steps: - name: "Test Model for Pull Requests" - uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release + uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: "pulls" cache_key: ${{ env.CACHE_KEY }} diff --git a/.github/workflows/labeler.md b/.github/workflows/labeler.md new file mode 100644 index 00000000000000..ff9339a5963e92 --- /dev/null +++ b/.github/workflows/labeler.md @@ -0,0 +1,40 @@ +# Issue-Labeler Workflows + +This repository uses actions from [dotnet/issue-labeler](https://github.com/dotnet/issue-labeler) to predict area labels for issues and pull requests. + +The following workflow templates were imported and updated from [dotnet/issue-labeler/wiki/Onboarding](https://github.com/dotnet/issue-labeler/wiki/Onboarding): + +1. `labeler-cache-retention.yml` +2. `labeler-predict-issues.yml` +3. `labeler-predict-pulls.yml` +4. `labeler-promote.yml` +5. `labeler-train.yml` + +## Repository Configuration + +Across these workflows, the following changes were made to configure the issue labeler for this repository: + +1. Set `LABEL_PREFIX` to `"area-"`: + - `labeler-predict-issues.yml` + - `labeler-predict-pulls.yml` + - `labeler-train.yml` +2. Set `DEFAULT_LABEL` to `"needs-area-label"`: + - `labeler-predict-issues.yml` + - `labeler-predict-pulls.yml` +3. Remove the `EXCLUDED_AUTHORS` value as we do not bypass labeling for any authors' issues/pulls in this repository: + - `labeler-predict-issues.yml` + - `labeler-predict-pulls.yml` +4. Update the pull request labeling branches to include `main` and `release/*`: + - `labeler-predict-pulls.yml` +5. Remove the `repository` input for training the models against another repository: + - `labeler-train.yml` +6. Update the cache retention cron schedule to an arbitrary time of day: + - `labeler-cache-retention.yml` + +## Repository Variables + +The following [repository variables](../../settings/variables/actions) can be configured to override default workflow behavior: + +| Variable | Description | Default | +|----------|-------------|---------| +| `ISSUE_LABELER_PREDICTION_THRESHOLD` | The minimum prediction confidence threshold for applying an area label. Predictions below this threshold will apply the `DEFAULT_LABEL` instead. | `0.05` | From b733fad7620fcd77288828e5295ce6c914466b27 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:49:17 -0700 Subject: [PATCH 40/74] one more try at the .github dir --- .github/actions/select-copilot-pat/README.md | 7 + .github/aw/actions-lock.json | 15 + .github/dependabot.yml | 2 + .github/policies/resourceManagement.yml | 16 + .github/skills/api-proposal/SKILL.md | 44 +- .../references/api-proposal-checklist.md | 11 + .github/workflows/breaking-change-doc.yml | 73 - .github/workflows/code-review.lock.yml | 10 +- .github/workflows/copilot-echo.lock.yml | 10 +- .../workflows/crossgen2-ci-triage.lock.yml | 1225 ----------------- .github/workflows/crossgen2-ci-triage.md | 329 ----- .github/workflows/labeler-cache-retention.yml | 2 +- .github/workflows/labeler-predict-issues.yml | 4 +- .github/workflows/labeler-predict-pulls.yml | 4 +- .github/workflows/labeler-promote.yml | 4 +- .github/workflows/labeler-train.yml | 12 +- .github/workflows/labeler.md | 40 - 17 files changed, 117 insertions(+), 1691 deletions(-) delete mode 100644 .github/workflows/breaking-change-doc.yml delete mode 100644 .github/workflows/crossgen2-ci-triage.lock.yml delete mode 100644 .github/workflows/crossgen2-ci-triage.md delete mode 100644 .github/workflows/labeler.md diff --git a/.github/actions/select-copilot-pat/README.md b/.github/actions/select-copilot-pat/README.md index 12eeafe5b1f817..8b57f6377bcc84 100644 --- a/.github/actions/select-copilot-pat/README.md +++ b/.github/actions/select-copilot-pat/README.md @@ -10,6 +10,11 @@ To use Agentic Workflows in a dotnet org repository: 2. Copy this `select-copilot-pat` folder into the repository under `.github/actions/select-copilot-pat`, including both the `README.md` and `action.yml`. 3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below. +> **Optional:** If you plan to manage secrets or workflows from the command line (e.g., `gh aw secrets set`), [install the `gh aw` CLI extension][cli-setup]: +> ```sh +> gh extension install github/gh-aw +> ``` + ## PAT Management Team members provide PATs into the pools for the repository by adding them as repository secrets with secret names matching the pattern of `_<0-9>`, such as `COPILOT_PAT_0`. @@ -85,6 +90,7 @@ engine: ## References +- [Agentic Workflows CLI Extension][cli-setup] - [Agentic Authoring][configure-repo] - [Authentication][authentication] - [Agentic Workflow Imports][imports] @@ -96,6 +102,7 @@ engine: - [Case Function in Workflow Expressions][case-expression] - [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override] +[cli-setup]: https://github.github.com/gh-aw/setup/cli/ [configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository [authentication]: https://github.github.com/gh-aw/reference/auth/ [create-pat]: https://github.com/settings/personal-access-tokens/new?name=dotnet%20org%20agentic%20workflows&description=GitHub+Agentic+Workflows+-+Copilot+engine+authentication.++Used+for+dotnet+org+workflows.+MUST+be+configured+with+only+Copilot+Requests+permissions+and+user+account+as+resource+owner.+Weekly+expiration+and+required+renewal.&user_copilot_requests=read&expires_in=8 diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 959efc4f8604ed..4ff4a7ea77497e 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -5,6 +5,11 @@ "version": "v8", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" }, + "actions/upload-artifact@v4": { + "repo": "actions/upload-artifact", + "version": "v4", + "sha": "ea165f8d65b6e75b540449e92b4886f43607fa02" + }, "github/gh-aw-actions/setup@v0.63.0": { "repo": "github/gh-aw-actions/setup", "version": "v0.63.0", @@ -14,6 +19,16 @@ "repo": "github/gh-aw-actions/setup", "version": "v0.63.1", "sha": "53e09ec0be6271e81a69f51ef93f37212c8834b0" + }, + "github/gh-aw-actions/setup@v0.64.5": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.64.5", + "sha": "5d2ebfd87a1a45a8a8323c1a12c01b055730dac5" + }, + "github/gh-aw-actions/setup@v0.65.6": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.65.6", + "sha": "31130b20a8fd3ef263acbe2091267c0aace07e09" } } } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 806f7fad67b045..e7c47261b8ebab 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,8 @@ updates: open-pull-requests-limit: 5 labels: - area-codeflow + exclude-paths: + - ".github/workflows/*.lock.yml" ignore: - dependency-name: "actions/checkout" update-types: ["version-update:semver-patch","version-update:semver-minor"] diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml index f192c82b6da5e5..05f03bb7612a09 100644 --- a/.github/policies/resourceManagement.yml +++ b/.github/policies/resourceManagement.yml @@ -146,6 +146,8 @@ configuration: label: area-Debugger-mono - labelAdded: label: area-DependencyModel + - labelAdded: + label: area-Diagnostics-cdac - labelAdded: label: area-Diagnostics-coreclr - labelAdded: @@ -490,6 +492,20 @@ configuration: See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed. assignMentionees: False - if: + - hasLabel: + label: area-Diagnostics-cdac + then: + - mentionUsers: + mentionees: + - steveisok + - tommcdon + - dotnet/dotnet-diag + replyTemplate: >- + Tagging subscribers to this area: ${mentionees} + + See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed. + assignMentionees: False + - if: - hasLabel: label: area-Diagnostics-coreclr then: diff --git a/.github/skills/api-proposal/SKILL.md b/.github/skills/api-proposal/SKILL.md index 8f1905b87d1428..80d1ca0d9d070e 100644 --- a/.github/skills/api-proposal/SKILL.md +++ b/.github/skills/api-proposal/SKILL.md @@ -113,7 +113,23 @@ The skill contains baked-in examples and guidelines for writing good proposals ( - Cover edge cases, null inputs, boundary conditions - Test any interaction with existing APIs -4. **Apply the new API throughout the runtime codebase** where relevant. Search (grep/ripgrep) for existing patterns that would benefit from the new API — e.g., manual workarounds, verbose boilerplate, or older idioms that the new API replaces. Include these call-site updates in the prototype commit to demonstrate real-world applicability and exercise the API in diverse contexts. +4. **Search dotnet/runtime for adoption sites.** Before writing any proposal text, systematically search the entire local repo (grep/ripgrep) for every place the new API could or should be used. This serves two purposes: validating that the API shape works in diverse real-world contexts, and producing the adoption catalog required in the proposal (see Phase 4, section 6). + + **What to search for:** + - Manual workarounds, verbose boilerplate, or older idioms that the new API directly replaces + - Call sites of the existing APIs being extended (e.g., if adding an overload, find every call to the current overloads) + - Patterns that would become simpler, more efficient, or more correct with the new API + - Third-party or test code within the repo that exercises the same scenario + + **How to catalog results:** + - Record every hit with file path and a one-line description of how the new API applies + - Classify each site into one of three categories: + - **Updated**: Already converted in the prototype commit (pick a representative set of the most diverse and impactful sites) + - **Candidate**: Could be converted but was intentionally deferred (e.g., different area, risk of churn, needs area-owner review) + - **Inapplicable**: Initially matched the search but doesn't actually benefit from the new API (brief reason why) + - The catalog should be thorough — err on the side of listing too many sites rather than too few + +5. **Apply the new API at representative sites.** From the catalog above, convert a diverse, representative set of adoption sites in the prototype commit. Prioritize sites that exercise different aspects of the API (different overloads, edge cases, interaction with other APIs). The remaining "Candidate" sites are listed in the proposal but left unconverted. #### Prototype Validation (all steps required) @@ -262,6 +278,32 @@ The agent has the burden of proof when claiming absence of risks. Evaluate: Write "No response" if there are genuinely no risks, matching the convention used in real `api-approved` issues. Do not inflate this section for straightforward additions. +**6. Usage in dotnet/runtime** + +Include the full adoption catalog produced during Phase 2, step 4. This section demonstrates that the API is broadly useful across the runtime codebase and helps area owners discover conversion opportunities in their code. + +Format as a table or grouped list: + +```markdown +#### Updated in prototype + +| File | Description | +|------|-------------| +| `src/libraries/System.Linq/src/System/Linq/Where.cs` | Replaced manual null-check + throw with `ArgumentNullException.ThrowIfNull` | +| `src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Queue.cs` | Replaced bounds-check boilerplate | + +#### Candidates for follow-up + +| File | Description | Why deferred | +|------|-------------|--------------| +| `src/coreclr/nativeaot/...` | Same pattern as above | Different area owner | +| `src/mono/...` | Uses equivalent Mono-specific helper | Needs area-owner review | +``` + +- Every "Updated" and "Candidate" site from the Phase 2 catalog must appear here. +- "Inapplicable" sites may be omitted from the proposal unless they illustrate an interesting design boundary (e.g., "this pattern looks like a match but isn't because X" — useful context for reviewers). +- If no adoption sites were found beyond the target library itself, state that explicitly — it's a signal reviewers will want to see. + #### After Drafting Present the complete draft to the user for review. Iterate based on feedback before publishing. diff --git a/.github/skills/api-proposal/references/api-proposal-checklist.md b/.github/skills/api-proposal/references/api-proposal-checklist.md index 88171bed7ed51e..e4612571580743 100644 --- a/.github/skills/api-proposal/references/api-proposal-checklist.md +++ b/.github/skills/api-proposal/references/api-proposal-checklist.md @@ -31,6 +31,17 @@ Use this checklist to validate an API proposal before publishing. Items are orde - [ ] **DO** consider overload consistency with existing method families - [ ] **DO NOT** propose a narrow addition without evaluating the broader scope — reviewers will ask about it +## Adoption Catalog (Usage in dotnet/runtime) + +- [ ] **DO** search the entire dotnet/runtime repo for every place the proposed API could or should be used +- [ ] **DO** search for manual workarounds, verbose boilerplate, and older idioms the new API replaces +- [ ] **DO** search for call sites of existing APIs being extended (e.g., all callers of the current overloads) +- [ ] **DO** classify each site as Updated, Candidate, or Inapplicable +- [ ] **DO** convert a diverse, representative subset of sites in the prototype to exercise the API in real contexts +- [ ] **DO** include the full catalog in the proposal (Phase 4, section 6 — "Usage in dotnet/runtime") +- [ ] **DO** explicitly state if no adoption sites were found outside the target library — this is a signal reviewers need +- [ ] **DO NOT** limit the search to the target library alone — search `src/libraries/`, `src/coreclr/`, `src/mono/`, and other areas + ## Prototype Validation - [ ] **DO** verify the prototype builds for all target frameworks in the library's `.csproj` diff --git a/.github/workflows/breaking-change-doc.yml b/.github/workflows/breaking-change-doc.yml deleted file mode 100644 index b115c38e729f30..00000000000000 --- a/.github/workflows/breaking-change-doc.yml +++ /dev/null @@ -1,73 +0,0 @@ -# This workflow generates breaking change documentation for merged pull requests. -# It runs automatically when a PR with the 'needs-breaking-change-doc-created' label is merged, -# or when that label is added to an already merged PR. -# It can be manually triggered to generate documentation for any specific PR. -# -# The workflow uses GitHub Models AI to analyze the PR changes and create appropriate -# breaking change documentation that gets posted as a PR comment as a clickable link -# to open an issue in the dotnet/docs repository. -name: Breaking Change Documentation - -on: - pull_request_target: - types: [closed, labeled] - workflow_dispatch: - inputs: - pr_number: - description: "Pull Request Number" - required: true - type: number - -permissions: - contents: read - pull-requests: write - models: read - -jobs: - generate-breaking-change-doc: - if: | - github.repository_owner == 'dotnet' && ( - (github.event_name == 'pull_request_target' && github.event.action == 'closed' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-breaking-change-doc-created')) || - (github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.pull_request.merged == true && github.event.label.name == 'needs-breaking-change-doc-created') || - github.event_name == 'workflow_dispatch' - ) - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 # Need full history for version detection - - - name: Verify PowerShell - run: | - pwsh --version - - - name: Verify GitHub CLI - run: | - gh --version - - - name: Install GitHub Models extension - run: | - gh extension install github/gh-models --force - env: - GH_TOKEN: ${{ github.token }} - - - name: Fetch latest tags - run: | - git fetch --tags --force - - - name: Run breaking change documentation script - shell: pwsh - working-directory: eng/breakingChanges - run: ./breaking-change-doc.ps1 -PrNumber ${{ inputs.pr_number || github.event.pull_request.number }} -Comment - env: - GH_TOKEN: ${{ github.token }} - GITHUB_MODELS_API_KEY: ${{ secrets.MODELS_TOKEN }} - - - name: Upload artifacts - uses: actions/upload-artifact@v7 - with: - name: breaking-change-doc-artifacts-${{ inputs.pr_number || github.event.pull_request.number }} - path: artifacts/docs/breakingChanges/ - retention-days: 7 diff --git a/.github/workflows/code-review.lock.yml b/.github/workflows/code-review.lock.yml index 9143fc2cb8717f..1bcee15bcd04d8 100644 --- a/.github/workflows/code-review.lock.yml +++ b/.github/workflows/code-review.lock.yml @@ -80,7 +80,7 @@ jobs: title: ${{ steps.sanitized.outputs.title }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@53e09ec0be6271e81a69f51ef93f37212c8834b0 # v0.63.1 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Generate agentic run info @@ -300,7 +300,7 @@ jobs: output_types: ${{ steps.collect_output.outputs.output_types }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@53e09ec0be6271e81a69f51ef93f37212c8834b0 # v0.63.1 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Set runtime paths @@ -917,7 +917,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@53e09ec0be6271e81a69f51ef93f37212c8834b0 # v0.63.1 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact @@ -1013,7 +1013,7 @@ jobs: select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@53e09ec0be6271e81a69f51ef93f37212c8834b0 # v0.63.1 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Check team membership for workflow @@ -1075,7 +1075,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@53e09ec0be6271e81a69f51ef93f37212c8834b0 # v0.63.1 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact diff --git a/.github/workflows/copilot-echo.lock.yml b/.github/workflows/copilot-echo.lock.yml index f4d97e2e509432..8fee66da4b665f 100644 --- a/.github/workflows/copilot-echo.lock.yml +++ b/.github/workflows/copilot-echo.lock.yml @@ -81,7 +81,7 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@9128d2542bbf1bdfec94dabeaf3e1d3c0d402577 # v0.63.0 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Generate agentic run info @@ -290,7 +290,7 @@ jobs: output_types: ${{ steps.collect_output.outputs.output_types }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@9128d2542bbf1bdfec94dabeaf3e1d3c0d402577 # v0.63.0 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Set runtime paths @@ -904,7 +904,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@9128d2542bbf1bdfec94dabeaf3e1d3c0d402577 # v0.63.0 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact @@ -999,7 +999,7 @@ jobs: select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@9128d2542bbf1bdfec94dabeaf3e1d3c0d402577 # v0.63.0 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Check team membership for workflow @@ -1061,7 +1061,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@9128d2542bbf1bdfec94dabeaf3e1d3c0d402577 # v0.63.0 + uses: github/gh-aw-actions/setup@536ea1bad8c6715d098a9dc1afea8d403733acfe # v0.65.4 with: destination: ${{ runner.temp }}/gh-aw/actions - name: Download agent output artifact diff --git a/.github/workflows/crossgen2-ci-triage.lock.yml b/.github/workflows/crossgen2-ci-triage.lock.yml deleted file mode 100644 index db03d9c5f946f0..00000000000000 --- a/.github/workflows/crossgen2-ci-triage.lock.yml +++ /dev/null @@ -1,1225 +0,0 @@ -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# This file was automatically generated by gh-aw (v0.64.2). DO NOT EDIT. -# -# To update this file, edit the corresponding .md file and run: -# gh aw compile -# Not all edits will cause changes to this file. -# -# For more information: https://github.github.com/gh-aw/introduction/overview/ -# -# Daily triage of crossgen2 CI pipeline failures - analyzes builds, creates issues, and assigns Copilot to fix or disable failing tests -# -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"d44a5e48fa6b2fc3f700bf3858f0bf05e8b9ca09bbdc310eb664b57cfc672c5e","compiler_version":"v0.64.2","strict":true,"agent_id":"copilot","agent_model":"claude-opus-4.6"} - -name: "Crossgen2 CI Failure Triage" -"on": - schedule: - - cron: "51 12 * * 1-5" - # Friendly format: daily on weekdays (scattered) - # steps: # Steps injected into pre-activation job - # - name: Checkout the select-copilot-pat action folder - # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - # with: - # fetch-depth: 1 - # persist-credentials: false - # sparse-checkout: .github/actions/select-copilot-pat - # sparse-checkout-cone-mode: true - # - env: - # SECRET_0: ${{ secrets.COPILOT_PAT_0 }} - # SECRET_1: ${{ secrets.COPILOT_PAT_1 }} - # SECRET_2: ${{ secrets.COPILOT_PAT_2 }} - # SECRET_3: ${{ secrets.COPILOT_PAT_3 }} - # SECRET_4: ${{ secrets.COPILOT_PAT_4 }} - # SECRET_5: ${{ secrets.COPILOT_PAT_5 }} - # SECRET_6: ${{ secrets.COPILOT_PAT_6 }} - # SECRET_7: ${{ secrets.COPILOT_PAT_7 }} - # SECRET_8: ${{ secrets.COPILOT_PAT_8 }} - # SECRET_9: ${{ secrets.COPILOT_PAT_9 }} - # id: select-copilot-pat - # name: Select Copilot token from pool - # uses: ./.github/actions/select-copilot-pat - workflow_dispatch: - inputs: - aw_context: - default: "" - description: Agent caller context (used internally by Agentic Workflows). - required: false - type: string - -permissions: {} - -concurrency: - group: "gh-aw-${{ github.workflow }}" - -run-name: "Crossgen2 CI Failure Triage" - -jobs: - activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} - model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - name: Generate agentic run info - id: generate_aw_info - env: - GH_AW_INFO_ENGINE_ID: "copilot" - GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: "claude-opus-4.6" - GH_AW_INFO_VERSION: "latest" - GH_AW_INFO_AGENT_VERSION: "latest" - GH_AW_INFO_CLI_VERSION: "v0.64.2" - GH_AW_INFO_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" - GH_AW_INFO_EXPERIMENTAL: "false" - GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" - GH_AW_INFO_STAGED: "false" - GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dev.azure.com","helix.dot.net","mihubot.xyz"]' - GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.25.1" - GH_AW_INFO_AWMG_VERSION: "" - GH_AW_INFO_FIREWALL_TYPE: "squid" - GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); - await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: ${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} - - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - sparse-checkout: | - .github - .agents - sparse-checkout-cone-mode: true - fetch-depth: 1 - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_WORKFLOW_FILE: "crossgen2-ci-triage.lock.yml" - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); - await main(); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - # poutine:ignore untrusted_checkout_exec - run: | - bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh - { - cat << 'GH_AW_PROMPT_cbb12d0a2eefb802_EOF' - - GH_AW_PROMPT_cbb12d0a2eefb802_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_cbb12d0a2eefb802_EOF' - - Tools: create_issue(max:10), missing_tool, missing_data, noop - - - The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ - {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_cbb12d0a2eefb802_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_cbb12d0a2eefb802_EOF' - - GH_AW_PROMPT_cbb12d0a2eefb802_EOF - cat << 'GH_AW_PROMPT_cbb12d0a2eefb802_EOF' - {{#runtime-import .github/workflows/crossgen2-ci-triage.md}} - GH_AW_PROMPT_cbb12d0a2eefb802_EOF - } > "$GH_AW_PROMPT" - - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_ALLOWED_EXTENSIONS: '' - GH_AW_CACHE_DESCRIPTION: '' - GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/' - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_ALLOWED_EXTENSIONS: process.env.GH_AW_ALLOWED_EXTENSIONS, - GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION, - GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR, - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, - GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, - GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, - GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED - } - }); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash ${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash ${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh - - name: Upload activation artifact - if: success() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: activation - path: | - /tmp/gh-aw/aw_info.json - /tmp/gh-aw/aw-prompts/prompt.txt - retention-days: 1 - - agent: - needs: activation - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - issues: read - pull-requests: read - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" - env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - GH_AW_ASSETS_ALLOWED_EXTS: "" - GH_AW_ASSETS_BRANCH: "" - GH_AW_ASSETS_MAX_SIZE_KB: 0 - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - GH_AW_WORKFLOW_ID_SANITIZED: crossgen2citriage - outputs: - checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - has_patch: ${{ steps.collect_output.outputs.has_patch }} - inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} - model: ${{ needs.activation.outputs.model }} - output: ${{ steps.collect_output.outputs.output }} - output_types: ${{ steps.collect_output.outputs.output_types }} - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - name: Set runtime paths - id: set-runtime-paths - run: | - echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" - echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" - echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Create gh-aw temp directory - run: bash ${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh - - name: Configure gh CLI for GitHub Enterprise - run: bash ${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh - env: - GH_TOKEN: ${{ github.token }} - # Cache memory file share configuration from frontmatter processed below - - name: Create cache-memory directory - run: bash ${RUNNER_TEMP}/gh-aw/actions/create_cache_memory_dir.sh - - name: Restore cache-memory file share data - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - key: memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} - path: /tmp/gh-aw/cache-memory - restore-keys: | - memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Checkout PR branch - id: checkout-pr - if: | - github.event.pull_request || github.event.issue.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); - await main(); - - name: Install GitHub Copilot CLI - run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest - env: - GH_HOST: github.com - - name: Install AWF binary - run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.1 - - name: Parse integrity filter lists - id: parse-guard-vars - env: - GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} - GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} - run: bash ${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh - - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.1 ghcr.io/github/gh-aw-firewall/squid:0.25.1 ghcr.io/github/gh-aw-mcpg:v0.2.6 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - - name: Write Safe Outputs Config - run: | - mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_4a4a87ef2c599b25_EOF' - {"create_issue":{"assignees":["copilot"],"expires":720,"labels":["area-CodeGen-coreclr"],"max":10,"title_prefix":"[Crossgen2 CI] "},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"}} - GH_AW_SAFE_OUTPUTS_CONFIG_4a4a87ef2c599b25_EOF - - name: Write Safe Outputs Tools - run: | - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_33af0b75731912b2_EOF' - { - "description_suffixes": { - "create_issue": " CONSTRAINTS: Maximum 10 issue(s) can be created. Title will be prefixed with \"[Crossgen2 CI] \". Labels [\"area-CodeGen-coreclr\"] will be automatically added. Assignees [\"copilot\"] will be automatically assigned." - }, - "repo_params": {}, - "dynamic_tools": [] - } - GH_AW_SAFE_OUTPUTS_TOOLS_META_33af0b75731912b2_EOF - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_e5513dbdb514de38_EOF' - { - "create_issue": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "parent": { - "issueOrPRNumber": true - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "temporary_id": { - "type": "string" - }, - "title": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "missing_data": { - "defaultMax": 20, - "fields": { - "alternatives": { - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "context": { - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "data_type": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "reason": { - "type": "string", - "sanitize": true, - "maxLength": 256 - } - } - }, - "missing_tool": { - "defaultMax": 20, - "fields": { - "alternatives": { - "type": "string", - "sanitize": true, - "maxLength": 512 - }, - "reason": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "tool": { - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "noop": { - "defaultMax": 1, - "fields": { - "message": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - } - } - } - } - GH_AW_SAFE_OUTPUTS_VALIDATION_e5513dbdb514de38_EOF - node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash ${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh - - - name: Start MCP Gateway - id: start-mcp-gateway - env: - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config - - # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" - export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY - export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" - mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" - export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" - export DEBUG="*" - - export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.6' - - mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_4fe7628130205168_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh - { - "mcpServers": { - "github": { - "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.32.0", - "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues,pull_requests,actions,search" - }, - "guard-policies": { - "allow-only": { - "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, - "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, - "min-integrity": "none", - "repos": "all" - } - } - }, - "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" - }, - "guard-policies": { - "write-sink": { - "accept": [ - "*" - ] - } - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" - } - } - GH_AW_MCP_CONFIG_4fe7628130205168_EOF - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - - name: Clean git credentials - continue-on-error: true - run: bash ${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 30 - run: | - set -o pipefail - touch /tmp/gh-aw/agent-step-summary.md - # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dev.azure.com,github.com,helix.dot.net,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,mihubot.xyz,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.1 --skip-pull --enable-api-proxy \ - -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: claude-opus-4.6 - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json - GH_AW_PHASE: agent - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.64.2 - GITHUB_API_URL: ${{ github.api_url }} - GITHUB_AW: true - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md - GITHUB_WORKSPACE: ${{ github.workspace }} - GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_AUTHOR_NAME: github-actions[bot] - GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] - XDG_CONFIG_HOME: /home/runner - - name: Detect inference access error - id: detect-inference-error - if: always() - continue-on-error: true - run: bash ${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Copy Copilot session state files to logs - if: always() - continue-on-error: true - run: | - # Copy Copilot session state files to logs folder for artifact collection - # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them - SESSION_STATE_DIR="$HOME/.copilot/session-state" - LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" - - if [ -d "$SESSION_STATE_DIR" ]; then - echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" - mkdir -p "$LOGS_DIR" - cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true - echo "Session state files copied successfully" - else - echo "No session-state directory found at $SESSION_STATE_DIR" - fi - - name: Stop MCP Gateway - if: always() - continue-on-error: true - env: - MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} - GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} - run: | - bash ${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); - await main(); - env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} - SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} - SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} - SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} - SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} - SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} - SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} - SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} - SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} - SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} - SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Append agent step summary - if: always() - run: bash ${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh - - name: Copy Safe Outputs - if: always() - env: - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - run: | - mkdir -p /tmp/gh-aw - cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true - - name: Ingest agent output - id: collect_output - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dev.azure.com,github.com,helix.dot.net,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,mihubot.xyz,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" - GH_AW_ALLOWED_GITHUB_REFS: "" - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_API_URL: ${{ github.api_url }} - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); - await main(); - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); - await main(); - - name: Parse MCP Gateway logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); - await main(); - - name: Print firewall logs - if: always() - continue-on-error: true - env: - AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi - - name: Write agent output placeholder if missing - if: always() - run: | - if [ ! -f /tmp/gh-aw/agent_output.json ]; then - echo '{"items":[]}' > /tmp/gh-aw/agent_output.json - fi - - name: Upload cache-memory data as artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - if: always() - with: - name: cache-memory - path: /tmp/gh-aw/cache-memory - - name: Upload agent artifacts - if: always() - continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: agent - path: | - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/proxy-logs/ - !/tmp/gh-aw/proxy-logs/proxy-tls/ - /tmp/gh-aw/agent-stdio.log - /tmp/gh-aw/agent/ - /tmp/gh-aw/safeoutputs.jsonl - /tmp/gh-aw/agent_output.json - /tmp/gh-aw/aw-*.patch - if-no-files-found: ignore - - name: Upload firewall audit logs - if: always() - continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: firewall-audit-logs - path: | - /tmp/gh-aw/sandbox/firewall/logs/ - /tmp/gh-aw/sandbox/firewall/audit/ - if-no-files-found: ignore - - conclusion: - needs: - - activation - - agent - - detection - - safe_outputs - - update_cache_memory - if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true') - runs-on: ubuntu-slim - permissions: - contents: read - issues: write - concurrency: - group: "gh-aw-conclusion-crossgen2-ci-triage" - cancel-in-progress: false - outputs: - noop_message: ${{ steps.noop.outputs.noop_message }} - tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} - total_count: ${{ steps.missing_tool.outputs.total_count }} - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: agent - path: /tmp/gh-aw/ - - name: Setup agent output environment variable - id: setup-agent-output-env - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/ - find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Process No-Op Messages - id: noop - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_NOOP_MAX: "1" - GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" - GH_AW_TRACKER_ID: "crossgen2-ci-triage" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/noop.cjs'); - await main(); - - name: Record Missing Tool - id: missing_tool - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" - GH_AW_TRACKER_ID: "crossgen2-ci-triage" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); - await main(); - - name: Handle Agent Failure - id: handle_agent_failure - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" - GH_AW_TRACKER_ID: "crossgen2-ci-triage" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_WORKFLOW_ID: "crossgen2-ci-triage" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} - GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} - GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} - GH_AW_ASSIGN_COPILOT_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.assign_copilot_failure_count }} - GH_AW_ASSIGN_COPILOT_ERRORS: ${{ needs.safe_outputs.outputs.assign_copilot_errors }} - GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} - GH_AW_GROUP_REPORTS: "false" - GH_AW_FAILURE_REPORT_AS_ISSUE: "true" - GH_AW_TIMEOUT_MINUTES: "30" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); - await main(); - - name: Handle No-Op Message - id: handle_noop_message - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" - GH_AW_TRACKER_ID: "crossgen2-ci-triage" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); - await main(); - - detection: - needs: agent - if: always() && needs.agent.result != 'skipped' - runs-on: ubuntu-latest - outputs: - detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} - detection_success: ${{ steps.detection_conclusion.outputs.success }} - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: agent - path: /tmp/gh-aw/ - - name: Setup agent output environment variable - id: setup-agent-output-env - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/ - find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - # --- Threat Detection --- - - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.1 ghcr.io/github/gh-aw-firewall/squid:0.25.1 - - name: Check if detection needed - id: detection_guard - if: always() - env: - OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - run: | - if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then - echo "run_detection=true" >> "$GITHUB_OUTPUT" - echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" - else - echo "run_detection=false" >> "$GITHUB_OUTPUT" - echo "Detection skipped: no agent outputs or patches to analyze" - fi - - name: Clear MCP configuration for detection - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - rm -f /tmp/gh-aw/mcp-config/mcp-servers.json - rm -f /home/runner/.copilot/mcp-config.json - rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - - name: Prepare threat detection files - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true - - name: Setup threat detection - if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - WORKFLOW_NAME: "Crossgen2 CI Failure Triage" - WORKFLOW_DESCRIPTION: "Daily triage of crossgen2 CI pipeline failures - analyzes builds, creates issues, and assigns Copilot to fix or disable failing tests" - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); - await main(); - - name: Ensure threat-detection directory and log - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - mkdir -p /tmp/gh-aw/threat-detection - touch /tmp/gh-aw/threat-detection/detection.log - - name: Install GitHub Copilot CLI - run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest - env: - GH_HOST: github.com - - name: Install AWF binary - run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.1 - - name: Execute GitHub Copilot CLI - if: always() && steps.detection_guard.outputs.run_detection == 'true' - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 - run: | - set -o pipefail - touch /tmp/gh-aw/agent-step-summary.md - # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.1 --skip-pull --enable-api-proxy \ - -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: claude-opus-4.6 - GH_AW_PHASE: detection - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.64.2 - GITHUB_API_URL: ${{ github.api_url }} - GITHUB_AW: true - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md - GITHUB_WORKSPACE: ${{ github.workspace }} - GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_AUTHOR_NAME: github-actions[bot] - GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] - XDG_CONFIG_HOME: /home/runner - - name: Upload threat detection log - if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: detection - path: /tmp/gh-aw/threat-detection/detection.log - if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - matched_command: '' - select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_REQUIRED_ROLES: "admin,maintainer,write" - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); - await main(); - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_PAT_0 }} - SECRET_1: ${{ secrets.COPILOT_PAT_1 }} - SECRET_2: ${{ secrets.COPILOT_PAT_2 }} - SECRET_3: ${{ secrets.COPILOT_PAT_3 }} - SECRET_4: ${{ secrets.COPILOT_PAT_4 }} - SECRET_5: ${{ secrets.COPILOT_PAT_5 }} - SECRET_6: ${{ secrets.COPILOT_PAT_6 }} - SECRET_7: ${{ secrets.COPILOT_PAT_7 }} - SECRET_8: ${{ secrets.COPILOT_PAT_8 }} - SECRET_9: ${{ secrets.COPILOT_PAT_9 }} - - safe_outputs: - needs: - - agent - - detection - if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' - runs-on: ubuntu-slim - permissions: - contents: read - issues: write - timeout-minutes: 15 - env: - GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/crossgen2-ci-triage" - GH_AW_ENGINE_ID: "copilot" - GH_AW_ENGINE_MODEL: "claude-opus-4.6" - GH_AW_TRACKER_ID: "crossgen2-ci-triage" - GH_AW_WORKFLOW_ID: "crossgen2-ci-triage" - GH_AW_WORKFLOW_NAME: "Crossgen2 CI Failure Triage" - outputs: - assign_copilot_errors: ${{ steps.assign_copilot_to_created_issues.outputs.assign_copilot_errors }} - assign_copilot_failure_count: ${{ steps.assign_copilot_to_created_issues.outputs.assign_copilot_failure_count }} - code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} - code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} - create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} - create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} - created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} - created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} - process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} - process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: agent - path: /tmp/gh-aw/ - - name: Setup agent output environment variable - id: setup-agent-output-env - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/ - find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - run: | - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Process Safe Outputs - id: process_safe_outputs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dev.azure.com,github.com,helix.dot.net,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,mihubot.xyz,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"assignees\":[\"copilot\"],\"expires\":720,\"labels\":[\"area-CodeGen-coreclr\"],\"max\":10,\"title_prefix\":\"[Crossgen2 CI] \"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"}}" - GH_AW_ASSIGN_COPILOT: "true" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); - await main(); - - name: Assign Copilot to created issues - id: assign_copilot_to_created_issues - if: steps.process_safe_outputs.outputs.issues_to_assign_copilot != '' - continue-on-error: true - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_ISSUES_TO_ASSIGN_COPILOT: ${{ steps.process_safe_outputs.outputs.issues_to_assign_copilot }} - with: - github-token: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/assign_copilot_to_created_issues.cjs'); - await main(); - - name: Upload Safe Output Items - if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: safe-output-items - path: /tmp/gh-aw/safe-output-items.jsonl - if-no-files-found: ignore - - update_cache_memory: - needs: - - agent - - detection - if: always() && needs.detection.result == 'success' - runs-on: ubuntu-latest - permissions: {} - env: - GH_AW_WORKFLOW_ID_SANITIZED: crossgen2citriage - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@f22886a9607f5c27e79742a8bfc5faa34737138b # v0.64.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - name: Download cache-memory artifact (default) - id: download_cache_default - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - continue-on-error: true - with: - name: cache-memory - path: /tmp/gh-aw/cache-memory - - name: Check if cache-memory folder has content (default) - id: check_cache_default - shell: bash - run: | - if [ -d "/tmp/gh-aw/cache-memory" ] && [ "$(ls -A /tmp/gh-aw/cache-memory 2>/dev/null)" ]; then - echo "has_content=true" >> "$GITHUB_OUTPUT" - else - echo "has_content=false" >> "$GITHUB_OUTPUT" - fi - - name: Save cache-memory to cache (default) - if: steps.check_cache_default.outputs.has_content == 'true' - uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - key: memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} - path: /tmp/gh-aw/cache-memory - diff --git a/.github/workflows/crossgen2-ci-triage.md b/.github/workflows/crossgen2-ci-triage.md deleted file mode 100644 index 4c1ccc4855a4cd..00000000000000 --- a/.github/workflows/crossgen2-ci-triage.md +++ /dev/null @@ -1,329 +0,0 @@ ---- -description: "Daily triage of crossgen2 CI pipeline failures - analyzes builds, creates issues, and assigns Copilot to fix or disable failing tests" - -on: - schedule: daily on weekdays - workflow_dispatch: - - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - - # Add the pre-activation step of selecting a random PAT from the supplied secrets - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_PAT_0 }} - SECRET_1: ${{ secrets.COPILOT_PAT_1 }} - SECRET_2: ${{ secrets.COPILOT_PAT_2 }} - SECRET_3: ${{ secrets.COPILOT_PAT_3 }} - SECRET_4: ${{ secrets.COPILOT_PAT_4 }} - SECRET_5: ${{ secrets.COPILOT_PAT_5 }} - SECRET_6: ${{ secrets.COPILOT_PAT_6 }} - SECRET_7: ${{ secrets.COPILOT_PAT_7 }} - SECRET_8: ${{ secrets.COPILOT_PAT_8 }} - SECRET_9: ${{ secrets.COPILOT_PAT_9 }} - -# Add the pre-activation output of the randomly selected PAT -jobs: - pre-activation: - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - -# Override the COPILOT_GITHUB_TOKEN expression used in the activation job -# Consume the PAT number from the pre-activation step and select the corresponding secret -engine: - id: copilot - model: claude-opus-4.6 - env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_PAT_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} - -timeout-minutes: 30 - -permissions: - contents: read - issues: read - actions: read - pull-requests: read - -tools: - github: - toolsets: [default, actions, search] - min-integrity: none - web-fetch: - cache-memory: true - -network: - allowed: - - defaults - - dev.azure.com - - helix.dot.net - - mihubot.xyz - -safe-outputs: - mentions: false - allowed-github-references: [] - create-issue: - max: 10 - assignees: [copilot] - labels: [area-CodeGen-coreclr] - title-prefix: "[Crossgen2 CI] " - expires: 30 - noop: - -tracker-id: crossgen2-ci-triage ---- - -# Crossgen2 CI Failure Triage - -You are an automated CI triage agent for the dotnet/runtime repository. Your job is to analyze recent failures in crossgen2-related CI pipelines, identify new unknown test failures, and create actionable GitHub issues assigned to Copilot Coding Agent. - -## Target Pipelines - -Analyze failures from these Azure DevOps pipelines (org: `dnceng-public`, project: `public`): - -1. `runtime-coreclr crossgen2` -2. `runtime-coreclr crossgen2-composite` -3. `runtime-coreclr crossgen2 outerloop` -4. `runtime-coreclr crossgen2-composite gcstress` - -### Cross-Reference Pipeline - -The following pipeline is used for cross-referencing failures (see Step 3): - -- `runtime` - -This pipeline is **not** a triage target — do not create issues for its failures. It is only queried when crossgen2 pipeline failures are found, to determine whether those failures also occur in the `runtime` pipeline (which indicates they are not crossgen2-specific). - -### Branch Restriction - -**All pipeline queries — both target pipelines and the cross-reference pipeline — MUST be filtered to the `main` branch only** (`branchName=refs/heads/main`). Do not analyze builds from pull request branches or any other branches. PR builds may have failures that will be fixed before merging to `main`. - -## Step 1: Discover Failed Builds - -Query Azure DevOps for builds completed in the last 48 hours (to cover weekends on Monday) that have failures. **Only query builds on the `main` branch — never analyze PR builds.** - -For each target pipeline: - -1. **Look up the pipeline definition ID**: - ``` - curl -s "https://dev.azure.com/dnceng-public/public/_apis/build/definitions?name=&api-version=7.0" - ``` - Extract the `id` field from the response. - -2. **Query failed builds** using the definition ID: - ``` - curl -s "https://dev.azure.com/dnceng-public/public/_apis/build/builds?definitions=&minTime=&resultFilter=failed&statusFilter=completed&branchName=refs/heads/main&api-version=7.0" - ``` - Use the current UTC time minus 48 hours for `minTime` in ISO 8601 format. The `branchName=refs/heads/main` filter is **required** — do not omit it. - -3. **Collect build IDs** for all failed builds across all four pipelines. - -If no failed builds are found across any target pipeline, call the `noop` safe output with a message explaining that no crossgen2 pipeline failures were found in the last 48 hours. - -## Step 2: Analyze Each Failed Build - -For each failed build, use the CI Analysis skill script: - -```bash -pwsh .github/skills/ci-analysis/scripts/Get-CIStatus.ps1 -BuildId -ShowLogs -SearchMihuBot -ContinueOnError -``` - -From the output, extract and preserve: -- **Failed job names** and their error categories -- **Specific test names**: Fully qualified test class and method names (e.g., `System.Net.Security.Tests.SslStreamTest.ConnectAsync_InvalidCertificate_Throws`) -- **Error messages and stack traces**: Copy exact error text from the CI output — these go directly into issue bodies -- **Helix work item details**: Work item names, error snippets, and console log URLs -- **Known issue matches** from Build Analysis -- **The `[CI_ANALYSIS_SUMMARY]` JSON block** for structured analysis - -**IMPORTANT**: Do not summarize or paraphrase error output. Copy the actual error messages, assertion failures, and stack traces verbatim from the CI analysis output. Issues must contain enough concrete detail for someone to understand the failure without re-running CI analysis. - -### Filtering Known Issues - -Skip failures that are already matched to known issues by Build Analysis. Focus only on **unknown/untracked failures** — these are the ones that need new issues. - -### Check Cache Memory - -Read from `cache-memory` a file named `triaged-builds.json` (if it exists). This contains build IDs and failure signatures that have already been triaged. Skip any failures that match entries in this file. - -## Step 3: Cross-Reference Failures Against the `runtime` Pipeline - -If Step 2 identified unknown, untracked crossgen2 failures, query the `runtime` pipeline to check whether those failures also occur there. **Skip this step entirely if there are no unknown crossgen2 failures to cross-reference.** - -First, discover failed `runtime` pipeline builds using the same approach as Step 1 (same time window, same `branchName=refs/heads/main` filter): - -1. Look up the pipeline definition ID for `runtime`. -2. Query failed builds on `main` in the last 48 hours. -3. Collect the `runtime` build IDs. - -Then, for each failed `runtime` pipeline build, run the CI Analysis script: - -```bash -pwsh .github/skills/ci-analysis/scripts/Get-CIStatus.ps1 -BuildId -ShowLogs -SearchMihuBot -ContinueOnError -``` - -Extract the failing test names from the `runtime` pipeline builds. Build a set of **runtime pipeline failure signatures** (fully qualified test names). - -Then compare the crossgen2 pipeline failures (from Step 2) against the runtime pipeline failures: - -- If a test failure from a crossgen2 pipeline **also appears in the `runtime` pipeline** (matching by fully qualified test name, regardless of error category or platform), mark that failure as a **runtime-shared failure**. -- **Do NOT create issues for runtime-shared failures.** These failures are not specific to crossgen2 and do not warrant new crossgen2 issues. -- Instead, collect all runtime-shared failures to be reported in the `noop` safe output (see Step 5). - -Only failures that are **unique to the crossgen2 pipelines** (i.e., not found in the `runtime` pipeline) should proceed to Step 4 and potentially have issues created. - -## Step 4: Search for Existing Issues - -For each unknown failure, search GitHub for existing issues that might already track it: - -1. **Search by test name**: Use GitHub search to find open issues mentioning the failing test name in `dotnet/runtime`: - - Search with the test class name and method name - - Check issues with labels `area-CodeGen-coreclr` or `Known Build Error` - -2. **Search by error signature**: If the test name search yields no results, search for distinctive parts of the error message. - -3. **Check MihuBot results**: The CI analysis script with `-SearchMihuBot` may have already found related issues — use those results. - -If an existing open issue already tracks the failure, skip creating a new one. Note the existing issue number in your analysis. - -## Step 5: Create Issues for New Failures - -For each genuinely new, untracked failure that is **unique to the crossgen2 pipelines** (not a runtime-shared failure from Step 3), create a GitHub issue using the `create-issue` safe output. - -### Assess Fix Complexity - -Before creating the issue, assess whether the failure looks **simply solvable**: - -**Simply solvable** (instruct Copilot to fix the root cause): -- An assertion message clearly indicates what value was expected vs actual -- A null reference exception with an obvious missing null check -- A simple type mismatch or casting error -- A race condition with an obvious synchronization fix -- The error message directly points to the fix - -**Not simply solvable** (instruct Copilot to disable the test): -- Complex logic failures requiring deep domain knowledge -- Intermittent/flaky failures without clear reproduction pattern -- Failures related to infrastructure or environment issues -- Crashes or timeouts without clear root cause -- Failures that require understanding complex crossgen2 internals - -### Issue Format - -Create issues with the following structure: - -**Title**: A concise description of the failing test (the `[Crossgen2 CI]` prefix is added automatically) - -**Body**: - -```markdown -### Failure Details - -- **Pipeline**: -- **Build**: [](https://dev.azure.com/dnceng-public/public/_build/results?buildId=) -- **Failed Tests**: List each failing test with its fully qualified name -- **Configuration**: -- **Error Category**: - -### Failing Tests - -List each failing test individually with its fully qualified name: - -| Test Name | Platform | Error Type | -|-----------|----------|------------| -| `Namespace.Class.Method` | linux-x64 | AssertionError / Timeout / Crash / etc. | - -### Error Output - -Include the **actual error messages and stack traces** from the CI analysis output. -Do NOT write "Helix console logs are not accessible" — instead include whatever error -text the CI analysis script DID return (assertion messages, exit codes, error lines). - -
-Error details - -\`\`\` - -\`\`\` - -
- -### Helix Details - -- **Job**: -- **Work Item**: -- **Console Log**: - -### Recommended Action - - - -**Option A (simple fix):** -The failure appears to be straightforward to fix. Please investigate and fix the root cause: -- -- - -**Option B (disable test):** -This failure requires deeper investigation. Please disable the failing test by adding an `[ActiveIssue]` attribute referencing this issue: -- Locate the test method or test class -- Add `[ActiveIssue("https://github.com/dotnet/runtime/issues/ISSUE_NUMBER")]` attribute -- If the test is in a `.csproj` with crossgen2-specific conditions, the disable may need to target specific configurations -``` - -For Option B (disabling tests), provide specific guidance: -- If you can identify the test source file path, mention it -- Suggest the correct `[ActiveIssue]` attribute syntax -- Note which configurations to disable for (e.g., only crossgen2, only specific OS) - -## Step 6: Update Cache Memory - -After processing all builds, write the updated `triaged-builds.json` to `cache-memory` with: -- Build IDs that were analyzed from the crossgen2 target pipelines -- Build IDs from the `runtime` cross-reference pipeline that were checked (to avoid re-analyzing them) -- Failure signatures (test name + error category) that were triaged -- Timestamp of this triage run - -Use filesystem-safe timestamp format `YYYY-MM-DD-HH-MM-SS` (no colons). - -## Important Guidelines - -- **Only analyze `main` branch builds.** Never analyze builds from pull request branches or feature branches. PR builds may contain failures that will be fixed before merging. -- **Do not create issues for failures that also occur in the `runtime` pipeline.** These are not crossgen2-specific. Instead, note them in the `noop` safe output. -- **Do not create duplicate issues.** Always search thoroughly before creating. -- **Do not create issues for known/tracked failures.** If Build Analysis has already matched a failure to a known issue, skip it. -- **Be conservative with "simple fix" assessments.** When in doubt, instruct Copilot to disable the test rather than attempt a fix. -- **Include specific test names and real error output in every issue.** Each issue MUST contain: - - Fully qualified test names (not just work item names like "GC" — drill into the specific test methods) - - Actual error messages, assertion text, or stack traces copied from the CI analysis output - - Do NOT say "Helix console logs are not accessible without authentication" as a substitute for error details. The CI analysis script already extracts error information — use it. -- **Include enough context in issues** for Copilot Coding Agent to act without further investigation. -- **Group related failures.** If the same test fails across multiple pipelines or configurations, create a single issue covering all occurrences. -- When calling the `noop` safe output, include: - - A summary of what was analyzed (which pipelines, how many builds) - - Any failures that were skipped because they also appear in the `runtime` pipeline (list the test names and note they are shared with `runtime`) - - Any failures that were skipped because they match known issues or cached entries diff --git a/.github/workflows/labeler-cache-retention.yml b/.github/workflows/labeler-cache-retention.yml index 2c4013895af0f3..1289f756b077b9 100644 --- a/.github/workflows/labeler-cache-retention.yml +++ b/.github/workflows/labeler-cache-retention.yml @@ -33,7 +33,7 @@ jobs: matrix: type: ["issues", "pulls"] steps: - - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: ${{ matrix.type }} cache_key: ${{ env.CACHE_KEY }} diff --git a/.github/workflows/labeler-predict-issues.yml b/.github/workflows/labeler-predict-issues.yml index 89cc11af409508..c206eb03952616 100644 --- a/.github/workflows/labeler-predict-issues.yml +++ b/.github/workflows/labeler-predict-issues.yml @@ -40,7 +40,7 @@ jobs: steps: - name: "Restore issues model from cache" id: restore-model - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: issues fail-on-cache-miss: ${{ env.ALLOW_FAILURE }} @@ -49,7 +49,7 @@ jobs: - name: "Predict issue labels" id: prediction if: ${{ steps.restore-model.outputs.cache-hit == 'true' }} - uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: issues: ${{ inputs.issues || github.event.issue.number }} label_prefix: ${{ env.LABEL_PREFIX }} diff --git a/.github/workflows/labeler-predict-pulls.yml b/.github/workflows/labeler-predict-pulls.yml index dc35e65490a108..30652b9106ec15 100644 --- a/.github/workflows/labeler-predict-pulls.yml +++ b/.github/workflows/labeler-predict-pulls.yml @@ -114,7 +114,7 @@ jobs: - name: "Restore pulls model from cache" id: restore-model if: ${{ steps.determine-pulls.outputs.pulls != '' }} - uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: pulls fail-on-cache-miss: ${{ env.ALLOW_FAILURE }} @@ -123,7 +123,7 @@ jobs: - name: "Predict pull labels" id: prediction if: ${{ steps.determine-pulls.outputs.pulls != '' && steps.restore-model.outputs.cache-hit == 'true' }} - uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: pulls: ${{ steps.determine-pulls.outputs.pulls }} label_prefix: ${{ env.LABEL_PREFIX }} diff --git a/.github/workflows/labeler-promote.yml b/.github/workflows/labeler-promote.yml index c01086c5177938..5f6770a75b4a0d 100644 --- a/.github/workflows/labeler-promote.yml +++ b/.github/workflows/labeler-promote.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Promote Model for Issues" - uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "issues" staged_key: ${{ inputs.staged_key }} @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Promote Model for Pull Requests" - uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/promote@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "pulls" staged_key: ${{ inputs.staged_key }} diff --git a/.github/workflows/labeler-train.yml b/.github/workflows/labeler-train.yml index d94630c9f44478..51b802c048862c 100644 --- a/.github/workflows/labeler-train.yml +++ b/.github/workflows/labeler-train.yml @@ -61,7 +61,7 @@ jobs: issues: read steps: - name: "Download Issues" - uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "issues" cache_key: ${{ env.CACHE_KEY }} @@ -80,7 +80,7 @@ jobs: pull-requests: read steps: - name: "Download Pull Requests" - uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/download@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "pulls" cache_key: ${{ env.CACHE_KEY }} @@ -99,7 +99,7 @@ jobs: needs: download-issues steps: - name: "Train Model for Issues" - uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "issues" data_cache_key: ${{ env.CACHE_KEY }} @@ -112,7 +112,7 @@ jobs: needs: download-pulls steps: - name: "Train Model for Pull Requests" - uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/train@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "pulls" data_cache_key: ${{ env.CACHE_KEY }} @@ -126,7 +126,7 @@ jobs: needs: train-issues steps: - name: "Test Model for Issues" - uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "issues" cache_key: ${{ env.CACHE_KEY }} @@ -147,7 +147,7 @@ jobs: needs: train-pulls steps: - name: "Test Model for Pull Requests" - uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 + uses: dotnet/issue-labeler/test@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0-release with: type: "pulls" cache_key: ${{ env.CACHE_KEY }} diff --git a/.github/workflows/labeler.md b/.github/workflows/labeler.md deleted file mode 100644 index ff9339a5963e92..00000000000000 --- a/.github/workflows/labeler.md +++ /dev/null @@ -1,40 +0,0 @@ -# Issue-Labeler Workflows - -This repository uses actions from [dotnet/issue-labeler](https://github.com/dotnet/issue-labeler) to predict area labels for issues and pull requests. - -The following workflow templates were imported and updated from [dotnet/issue-labeler/wiki/Onboarding](https://github.com/dotnet/issue-labeler/wiki/Onboarding): - -1. `labeler-cache-retention.yml` -2. `labeler-predict-issues.yml` -3. `labeler-predict-pulls.yml` -4. `labeler-promote.yml` -5. `labeler-train.yml` - -## Repository Configuration - -Across these workflows, the following changes were made to configure the issue labeler for this repository: - -1. Set `LABEL_PREFIX` to `"area-"`: - - `labeler-predict-issues.yml` - - `labeler-predict-pulls.yml` - - `labeler-train.yml` -2. Set `DEFAULT_LABEL` to `"needs-area-label"`: - - `labeler-predict-issues.yml` - - `labeler-predict-pulls.yml` -3. Remove the `EXCLUDED_AUTHORS` value as we do not bypass labeling for any authors' issues/pulls in this repository: - - `labeler-predict-issues.yml` - - `labeler-predict-pulls.yml` -4. Update the pull request labeling branches to include `main` and `release/*`: - - `labeler-predict-pulls.yml` -5. Remove the `repository` input for training the models against another repository: - - `labeler-train.yml` -6. Update the cache retention cron schedule to an arbitrary time of day: - - `labeler-cache-retention.yml` - -## Repository Variables - -The following [repository variables](../../settings/variables/actions) can be configured to override default workflow behavior: - -| Variable | Description | Default | -|----------|-------------|---------| -| `ISSUE_LABELER_PREDICTION_THRESHOLD` | The minimum prediction confidence threshold for applying an area label. Predictions below this threshold will apply the `DEFAULT_LABEL` instead. | `0.05` | From 8b63902dec6e06ab6152c2e346b3ee02632562d7 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:53:41 -0700 Subject: [PATCH 41/74] Extract EnsureDefTokensAreAvailable and related method to ExternalReferenceTokenManager --- .../Compiler/ExternalReferenceTokenManager.cs | 182 ++++++++++++++++++ .../Compiler/ReadyToRunCodegenCompilation.cs | 165 +++------------- .../ILCompiler.ReadyToRun.csproj | 1 + 3 files changed, 205 insertions(+), 143 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs new file mode 100644 index 00000000000000..f32a2bb985eec9 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using Internal.IL; +using Internal.IL.Stubs; +using Internal.JitInterface; +using Internal.ReadyToRunConstants; +using Internal.TypeSystem; + +using ILCompiler.DependencyAnalysis; +using ILCompiler.DependencyAnalysis.ReadyToRun; +using ILCompiler.DependencyAnalysisFramework; +using ILCompiler.Reflection.ReadyToRun; +using Internal.TypeSystem.Ecma; +using ILCompiler.ReadyToRun.TypeSystem; + +namespace ILCompiler.ReadyToRun +{ + /// + /// Handles the creation of IL tokens necessary for creating ReadyToRun signatures for types, methods, and fields not already present in the input modules within the version bubble. + /// + internal class ExternalReferenceTokenManager + { + private MutableModule _mutableModule; + private ModuleTokenResolver _tokenResolver; + + public ExternalReferenceTokenManager(MutableModule mutableModule, ModuleTokenResolver tokenResolver) + { + _mutableModule = mutableModule; + _tokenResolver = tokenResolver; + } + + /// + /// Ensures that all the tokens necessary for creating a ReadyToRun signature for the given entity are present in the MutableModule. + /// Adds the necessary tokens for the given entity to the MutableModule if they are not already present. + /// + public void EnsureDefTokensAreAvailable(IEnumerable entities, ModuleDesc moduleForNewReferences) + { + _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = moduleForNewReferences; + foreach (var entity in entities) + { + EnsureDefTokensAreAvailableInternal(entity); + } + _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = null; + } + + /// + /// Ensures that all the tokens necessary for creating a ReadyToRun signature for the given entity are present in the MutableModule. + /// Adds the necessary tokens for the given entity to the MutableModule if they are not already present. + /// + public void EnsureDefTokensAreAvailable(TypeSystemEntity entity, ModuleDesc moduleForNewReferences) + { + + _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = moduleForNewReferences; + try + { + EnsureDefTokensAreAvailableInternal(entity); + } + finally + { + _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = null; + } + } + + private void EnsureDefTokensAreAvailableInternal(TypeSystemEntity entity) + { + Debug.Assert(_mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences != null); + switch (entity) + { + case TypeDesc typeDesc: + EnsureTypeDefTokensAreAvailableInVersionBubble(typeDesc); + break; + case MethodDesc methodDesc: + EnsureMethodDefTokensAreAvailableInVersionBubble(methodDesc); + break; + case FieldDesc fieldDesc: + EnsureFieldDefTokensAreAvailableInVersionBubble(fieldDesc); + break; + default: + throw new NotSupportedException(); + }; + } + + private void AddTokenToMutableModule(TypeSystemEntity entity) + { + var existingToken = entity switch + { + TypeDesc typeDesc => _tokenResolver.GetModuleTokenForType(typeDesc, allowDynamicallyCreatedReference: true, throwIfNotFound: false), + MethodDesc methodDesc => _tokenResolver.GetModuleTokenForMethod(methodDesc, allowDynamicallyCreatedReference: true, throwIfNotFound: false), + FieldDesc fieldDesc => _tokenResolver.GetModuleTokenForField(fieldDesc, allowDynamicallyCreatedReference: true, throwIfNotFound: false), + _ => throw new NotSupportedException() + }; + + if (!existingToken.IsNull) + return; + + if (!_mutableModule.TryGetEntityHandle(entity).HasValue) + { + throw new InternalCompilerErrorException($"Unable to create token to {entity}"); + } + } + + private void EnsureMethodDefTokensAreAvailableInVersionBubble(MethodDesc methodDesc) + { + if (!methodDesc.IsPrimaryMethodDesc()) + { + EnsureMethodDefTokensAreAvailableInVersionBubble(methodDesc.GetPrimaryMethodDesc()); + return; + } + if (methodDesc is EcmaMethod ecmaMethod) + { + AddTokenToMutableModule(ecmaMethod); + return; + } + if (methodDesc.HasInstantiation) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(methodDesc.GetMethodDefinition().OwningType); + foreach (TypeDesc instParam in methodDesc.Instantiation) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(instParam); + } + EnsureMethodDefTokensAreAvailableInVersionBubble(methodDesc.GetTypicalMethodDefinition()); + } + } + + private void EnsureFieldDefTokensAreAvailableInVersionBubble(FieldDesc fieldDesc) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(fieldDesc.OwningType); + EnsureTypeDefTokensAreAvailableInVersionBubble(fieldDesc.FieldType); + if (fieldDesc is EcmaField ecmaField) + { + AddTokenToMutableModule(ecmaField); + } + else + { + EnsureFieldDefTokensAreAvailableInVersionBubble(fieldDesc.GetTypicalFieldDefinition()); + } + } + + private void EnsureTypeDefTokensAreAvailableInVersionBubble(TypeDesc type) + { + // Type represented by simple element type + if (type.IsPrimitive || type.IsVoid || type.IsObject || type.IsString || type.IsTypedReference) + return; + + if (type is EcmaType ecmaType) + { + AddTokenToMutableModule(ecmaType); + return; + } + if (type is ParameterizedType parameterizedType) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(parameterizedType.ParameterType); + AddTokenToMutableModule(parameterizedType); + return; + } + + if (type.HasInstantiation) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(type.GetTypeDefinition()); + + foreach (TypeDesc instParam in type.Instantiation) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(instParam); + } + } + else if (type.IsParameterizedType) + { + EnsureTypeDefTokensAreAvailableInVersionBubble(type.GetParameterType()); + } + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs index 4ff91ca9bd6caf..18893646862dff 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs @@ -19,6 +19,7 @@ using ILCompiler.DependencyAnalysis; using ILCompiler.DependencyAnalysis.ReadyToRun; using ILCompiler.DependencyAnalysisFramework; +using ILCompiler.ReadyToRun; using ILCompiler.Reflection.ReadyToRun; using Internal.TypeSystem.Ecma; using ILCompiler.ReadyToRun.TypeSystem; @@ -315,6 +316,8 @@ public sealed class ReadyToRunCodegenCompilation : Compilation private ConcurrentDictionary _computedFixedLayoutTypes = new ConcurrentDictionary(); private Func _computedFixedLayoutTypesUncached; + private readonly ExternalReferenceTokenManager _tokenManager; + internal ReadyToRunCodegenCompilation( DependencyAnalyzerBase dependencyGraph, NodeFactory nodeFactory, @@ -387,6 +390,7 @@ internal ReadyToRunCodegenCompilation( _profileData = profileData; _fileLayoutOptimizer = new FileLayoutOptimizer(logger, methodLayoutAlgorithm, fileLayoutAlgorithm, profileData, _nodeFactory); + _tokenManager = new ExternalReferenceTokenManager(_nodeFactory.ManifestMetadataTable._mutableModule, _nodeFactory.Resolver); } private readonly static string s_folderUpPrefix = ".." + Path.DirectorySeparatorChar; @@ -728,10 +732,11 @@ protected override void ComputeDependencyNodeDependencies(List { switch(il.GetObject(tok)) { - case string: - break; case TypeSystemEntity tse: - EnsureDefTokensAreAvailable(tse, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module); + _tokenManager.EnsureDefTokensAreAvailable(tse, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module); + break; + default: + // We don't need to worry about string handles break; } return tok; }); + // ILTokenReplacer doesn't handle exception regions or local variable types, so handle those separately var exceptionRegions = (ILExceptionRegion[])il.GetExceptionRegions(); for (int i = 0; i < exceptionRegions.Length; i++) { @@ -807,9 +815,13 @@ void EnsureAsyncThunkTokensAreAvailable(MethodDesc method) if (region.Kind == ILExceptionRegionKind.Catch) { TypeSystemEntity catchType = (TypeSystemEntity)il.GetObject(region.ClassToken); - EnsureDefTokensAreAvailable(catchType, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module); + _tokenManager.EnsureDefTokensAreAvailable(catchType, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module); } } + foreach (var local in il.GetLocals()) + { + _tokenManager.EnsureDefTokensAreAvailable(local.Type, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module); + } } finally { @@ -1001,9 +1013,9 @@ private void EnsureInstantiationReferencesArePresentForExternalMethod(MethodDesc // have tokens. var moduleForNewReferences = ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module; foreach (var type in method.Instantiation) - EnsureTypeDefTokensAreAvailableInVersionBubble(type, moduleForNewReferences); + _tokenManager.EnsureDefTokensAreAvailable(type, moduleForNewReferences); foreach (var type in method.OwningType.Instantiation) - EnsureTypeDefTokensAreAvailableInVersionBubble(type, moduleForNewReferences); + _tokenManager.EnsureDefTokensAreAvailable(type, moduleForNewReferences); } private void AddNecessaryAsyncReferences(MethodDesc method) @@ -1039,143 +1051,10 @@ private void AddNecessaryAsyncReferences(MethodDesc method) asyncHelpers.GetKnownMethod("AllocContinuationMethod"u8, null), ]; var moduleForNewReferences = ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module; - EnsureDefTokensAreAvailable(requiredMethods, moduleForNewReferences); - EnsureDefTokensAreAvailable(requiredTypes, moduleForNewReferences); - EnsureDefTokensAreAvailable(requiredFields, moduleForNewReferences); + _tokenManager.EnsureDefTokensAreAvailable([..requiredMethods, ..requiredTypes, ..requiredFields], moduleForNewReferences); _hasAddedAsyncReferences = true; } - void EnsureDefTokensAreAvailable(IEnumerable entities, ModuleDesc moduleForNewReferences) - { - foreach (var entity in entities) - { - EnsureDefTokensAreAvailable(entity, moduleForNewReferences); - } - } - - void EnsureDefTokensAreAvailable(TypeSystemEntity entity, ModuleDesc moduleForNewReferences) - { - switch (entity) - { - case TypeDesc typeDesc: - EnsureTypeDefTokensAreAvailableInVersionBubble(typeDesc, moduleForNewReferences); - break; - case MethodDesc methodDesc: - EnsureMethodDefTokensAreAvailableInVersionBubble(methodDesc, moduleForNewReferences); - break; - case FieldDesc fieldDesc: - EnsureFieldDefTokensAreAvailableInVersionBubble(fieldDesc, moduleForNewReferences); - break; - default: - throw new NotSupportedException(); - }; - } - - private void AddTokenToMutableModule(TypeSystemEntity entity, ModuleDesc module) - { - var existingToken = entity switch - { - TypeDesc typeDesc => _nodeFactory.Resolver.GetModuleTokenForType(typeDesc, allowDynamicallyCreatedReference: true, throwIfNotFound: false), - MethodDesc methodDesc => _nodeFactory.Resolver.GetModuleTokenForMethod(methodDesc, allowDynamicallyCreatedReference: true, throwIfNotFound: false), - FieldDesc fieldDesc => _nodeFactory.Resolver.GetModuleTokenForField(fieldDesc, allowDynamicallyCreatedReference: true, throwIfNotFound: false), - _ => throw new NotSupportedException() - }; - - var declaringModule = entity switch - { - TypeDesc ecmaType => (ecmaType.GetTypeDefinition() as EcmaType)?.Module, - MethodDesc ecmaMethod => ((EcmaMethod)ecmaMethod.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, - EcmaField ecmaField => ecmaField.Module, - _ => null - }; - - if (!existingToken.IsNull) - return; - - try - { - _nodeFactory.ManifestMetadataTable._mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = module; - if (!_nodeFactory.ManifestMetadataTable._mutableModule.TryGetEntityHandle(entity).HasValue) - { - throw new InternalCompilerErrorException($"Unable to create token to {entity}"); - } - } - finally - { - _nodeFactory.ManifestMetadataTable._mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = null; - } - } - - private void EnsureMethodDefTokensAreAvailableInVersionBubble(MethodDesc methodDesc, ModuleDesc moduleForNewReferences) - { - if (!methodDesc.IsPrimaryMethodDesc()) - { - EnsureMethodDefTokensAreAvailableInVersionBubble(methodDesc.GetPrimaryMethodDesc(), moduleForNewReferences); - return; - } - if (methodDesc is EcmaMethod ecmaMethod) - { - AddTokenToMutableModule(ecmaMethod, ecmaMethod.Module); - return; - } - if (methodDesc.HasInstantiation) - { - EnsureTypeDefTokensAreAvailableInVersionBubble(methodDesc.GetMethodDefinition().OwningType, moduleForNewReferences); - foreach (TypeDesc instParam in methodDesc.Instantiation) - { - EnsureTypeDefTokensAreAvailableInVersionBubble(instParam, moduleForNewReferences); - } - EnsureMethodDefTokensAreAvailableInVersionBubble(methodDesc.GetTypicalMethodDefinition(), moduleForNewReferences); - } - } - - private void EnsureFieldDefTokensAreAvailableInVersionBubble(FieldDesc fieldDesc, ModuleDesc moduleForNewReferences) - { - EnsureTypeDefTokensAreAvailableInVersionBubble(fieldDesc.OwningType, moduleForNewReferences); - EnsureTypeDefTokensAreAvailableInVersionBubble(fieldDesc.FieldType, moduleForNewReferences); - if (fieldDesc is EcmaField ecmaField) - { - AddTokenToMutableModule(ecmaField, ecmaField.Module); - } - else - { - EnsureFieldDefTokensAreAvailableInVersionBubble(fieldDesc.GetTypicalFieldDefinition(), moduleForNewReferences); - } - } - - private void EnsureTypeDefTokensAreAvailableInVersionBubble(TypeDesc type, ModuleDesc moduleForNewReferences) - { - // Type represented by simple element type - if (type.IsPrimitive || type.IsVoid || type.IsObject || type.IsString || type.IsTypedReference) - return; - - if (type is EcmaType ecmaType) - { - AddTokenToMutableModule(ecmaType, ecmaType.Module); - return; - } - if (type is ParameterizedType parameterizedType) - { - EnsureTypeDefTokensAreAvailableInVersionBubble(parameterizedType.ParameterType, moduleForNewReferences); - AddTokenToMutableModule(parameterizedType, moduleForNewReferences); - return; - } - - if (type.HasInstantiation) - { - EnsureTypeDefTokensAreAvailableInVersionBubble(type.GetTypeDefinition(), moduleForNewReferences); - - foreach (TypeDesc instParam in type.Instantiation) - { - EnsureTypeDefTokensAreAvailableInVersionBubble(instParam, moduleForNewReferences); - } - } - else if (type.IsParameterizedType) - { - EnsureTypeDefTokensAreAvailableInVersionBubble(type.GetParameterType(), moduleForNewReferences); - } - } - public ISymbolNode GetFieldRvaData(FieldDesc field) { if (!CompilationModuleGroup.ContainsType(field.OwningType.GetTypeDefinition())) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj index e021d5d7afc985..04225c7f4fcfc4 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj @@ -304,6 +304,7 @@ + From f2704fb0ca95e446ffab13da2996e94071d30844 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:23:41 -0700 Subject: [PATCH 42/74] Add details to stale IL-CG2 warning --- src/tests/Common/tests.targets | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/Common/tests.targets b/src/tests/Common/tests.targets index 335728c416ecb5..dd90fa598beeea 100644 --- a/src/tests/Common/tests.targets +++ b/src/tests/Common/tests.targets @@ -222,10 +222,10 @@ <_StaleIlCg2DoneFile Include="@(_IlCg2DoneFiles)" - Condition="$([System.IO.File]::GetLastWriteTime('%(Identity)').Ticks) < $(_Crossgen2InCoreRootTime)" /> + Condition="'@(_IlCg2DoneFiles)' != '' and ($([System.IO.File]::GetLastWriteTime('%(Identity)').Ticks) < $(_Crossgen2InCoreRootTime))" /> - From 9fc88fa59cf81672d416b0e77f443b3cccc131bf Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:41:40 -0700 Subject: [PATCH 43/74] Address copilot feedback --- .../tools/Common/JitInterface/CorInfoImpl.cs | 2 +- .../IL/ReadyToRunILProvider.cs | 4 ++-- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 24 +++++++++++-------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index f167e8f53efc73..ff11ffb9f71ed7 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -1872,7 +1872,7 @@ private void resolveToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken) else { recordToken = (_compilation.CompilationModuleGroup.VersionsWithType(owningType) || _compilation.CompilationModuleGroup.CrossModuleInlineableType(owningType)) && owningType is EcmaType; - recordToken &= !methodIL.OwningMethod.IsCompilerGeneratedILBodyForAsync(); + recordToken &= methodIL.GetMethodILScopeDefinition() is IMethodTokensAreUseableInCompilation || methodIL.GetMethodILScopeDefinition() is IEcmaMethodIL; } #endif diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs index 43190b95252fe2..448b25e7cae86c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs @@ -177,7 +177,7 @@ public bool NeedsCrossModuleInlineableTokens(MethodDesc method) return false; } - public static bool NeedsTaskReturningThunk(MethodDesc method) + private static bool NeedsTaskReturningThunk(MethodDesc method) { Debug.Assert(method.IsTypicalMethodDefinition); if (method is not EcmaMethod ecmaMethod) @@ -195,7 +195,7 @@ public static bool NeedsTaskReturningThunk(MethodDesc method) return false; } - public static bool NeedsAsyncThunk(MethodDesc method) + private static bool NeedsAsyncThunk(MethodDesc method) { Debug.Assert(method.IsTypicalMethodDefinition); if (method is not AsyncMethodVariant) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index ebde54c95f6d8d..2bf566e770cca1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -56,7 +56,7 @@ public class FieldWithToken : IEquatable public readonly ModuleToken Token; public readonly bool OwningTypeNotDerivedFromToken; - public FieldWithToken(FieldDesc field, ModuleToken token) + public FieldWithToken(FieldDesc field, ModuleToken token, bool forceOwningTypeNotDerivedFromToken = false) { Field = field; Token = token; @@ -75,6 +75,10 @@ public FieldWithToken(FieldDesc field, ModuleToken token) break; } } + if (forceOwningTypeNotDerivedFromToken) + { + OwningTypeNotDerivedFromToken = true; + } } public override bool Equals(object obj) @@ -330,10 +334,12 @@ public bool Equals(MethodWithToken methodWithToken) if (methodWithToken == null) return false; - bool equals = Method == methodWithToken.Method && Token.Equals(methodWithToken.Token) - && OwningType == methodWithToken.OwningType && ConstrainedType == methodWithToken.ConstrainedType + bool equals = Method == methodWithToken.Method + && Token.Equals(methodWithToken.Token) + && OwningType == methodWithToken.OwningType + && ConstrainedType == methodWithToken.ConstrainedType && Unboxing == methodWithToken.Unboxing - && OwningTypeNotDerivedFromToken == methodWithToken.OwningTypeNotDerivedFromToken && OwningType == methodWithToken.OwningType; + && OwningTypeNotDerivedFromToken == methodWithToken.OwningTypeNotDerivedFromToken; return equals; } @@ -1384,8 +1390,8 @@ private bool canTailCall(CORINFO_METHOD_STRUCT_* callerHnd, CORINFO_METHOD_STRUC private FieldWithToken ComputeFieldWithToken(FieldDesc field, ref CORINFO_RESOLVED_TOKEN pResolvedToken) { - ModuleToken token = HandleToModuleToken(ref pResolvedToken, out _); - return new FieldWithToken(field, token); + ModuleToken token = HandleToModuleToken(ref pResolvedToken, out bool strippedInstantiation); + return new FieldWithToken(field, token, strippedInstantiation); } private MethodWithToken ComputeMethodWithToken(MethodDesc method, ref CORINFO_RESOLVED_TOKEN pResolvedToken, TypeDesc constrainedType, bool unboxing) @@ -1398,7 +1404,7 @@ private MethodWithToken ComputeMethodWithToken(MethodDesc method, ref CORINFO_RE devirtualizedMethodOwner = HandleToObject(pResolvedToken.hClass); } - // For crossgen-generated il stubs, we may only have access to the method token for the typical method defition. + // For crossgen-generated IL stubs, we may only have access to the method token for the typical method definition. // This means that the owning type of the method may not be encoded in the method token. // bool methodTokenMightNotEncodeOwningType = MethodBeingCompiled.IsCompilerGeneratedILBodyForAsync(); return new MethodWithToken(method, token, constrainedType: constrainedType, unboxing: unboxing, genericContextObject: context, devirtualizedMethodOwner: devirtualizedMethodOwner, forceOwningTypeFromMethodDesc: strippedInstantiation); @@ -1433,9 +1439,7 @@ ModuleToken _HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken, Meth context = entityFromContext(pResolvedToken.tokenContext); } - var x = HandleToModuleToken(ref pResolvedToken, out bool s); - strippedInstantiation = s; - return x; + return HandleToModuleToken(ref pResolvedToken, out strippedInstantiation); } } From 1aa38f9eb9ae10b7656c2fa5dda2f644a2573906 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:50:12 -0700 Subject: [PATCH 44/74] Fix PR comments --- .../Compiler/ExternalReferenceTokenManager.cs | 38 +++++++++---------- .../Compiler/ReadyToRunCodegenCompilation.cs | 18 ++++++--- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs index f32a2bb985eec9..0c86a5754f3f9e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs @@ -2,26 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using Internal.IL; -using Internal.IL.Stubs; -using Internal.JitInterface; -using Internal.ReadyToRunConstants; -using Internal.TypeSystem; - -using ILCompiler.DependencyAnalysis; using ILCompiler.DependencyAnalysis.ReadyToRun; -using ILCompiler.DependencyAnalysisFramework; -using ILCompiler.Reflection.ReadyToRun; -using Internal.TypeSystem.Ecma; using ILCompiler.ReadyToRun.TypeSystem; +using Internal.TypeSystem; +using Internal.TypeSystem.Ecma; namespace ILCompiler.ReadyToRun { @@ -43,24 +29,33 @@ public ExternalReferenceTokenManager(MutableModule mutableModule, ModuleTokenRes /// Ensures that all the tokens necessary for creating a ReadyToRun signature for the given entity are present in the MutableModule. /// Adds the necessary tokens for the given entity to the MutableModule if they are not already present. /// - public void EnsureDefTokensAreAvailable(IEnumerable entities, ModuleDesc moduleForNewReferences) + public void EnsureDefTokensAreAvailable(IEnumerable entities, ModuleDesc moduleForNewReferences, bool referencesAreForAsyncMethod) { _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = moduleForNewReferences; - foreach (var entity in entities) + _mutableModule.CreatingTokensForAsyncMethod = referencesAreForAsyncMethod; + try { - EnsureDefTokensAreAvailableInternal(entity); + foreach (var entity in entities) + { + EnsureDefTokensAreAvailableInternal(entity); + } + } + finally + { + _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = null; + _mutableModule.CreatingTokensForAsyncMethod = false; } - _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = null; } /// /// Ensures that all the tokens necessary for creating a ReadyToRun signature for the given entity are present in the MutableModule. /// Adds the necessary tokens for the given entity to the MutableModule if they are not already present. /// - public void EnsureDefTokensAreAvailable(TypeSystemEntity entity, ModuleDesc moduleForNewReferences) + public void EnsureDefTokensAreAvailable(TypeSystemEntity entity, ModuleDesc moduleForNewReferences, bool referencesAreForAsyncMethod) { _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = moduleForNewReferences; + _mutableModule.CreatingTokensForAsyncMethod = referencesAreForAsyncMethod; try { EnsureDefTokensAreAvailableInternal(entity); @@ -68,6 +63,7 @@ public void EnsureDefTokensAreAvailable(TypeSystemEntity entity, ModuleDesc modu finally { _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = null; + _mutableModule.CreatingTokensForAsyncMethod = false; } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs index 18893646862dff..ed1b1698bc0e0a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs @@ -789,6 +789,12 @@ void EnsureAsyncThunkTokensAreAvailable(MethodDesc method) MethodIL il = null!; var methodDef = method.GetTypicalMethodDefinition(); il = ilProvider.GetMethodIL(methodDef); + // We shouldn't get null IL, but just in case, handle it gracefully + Debug.Assert(il is not null); + if (il is null) + { + return; + } try { NodeFactory.ManifestMetadataTable._mutableModule.CreatingTokensForAsyncMethod = true; @@ -799,7 +805,7 @@ void EnsureAsyncThunkTokensAreAvailable(MethodDesc method) switch(il.GetObject(tok)) { case TypeSystemEntity tse: - _tokenManager.EnsureDefTokensAreAvailable(tse, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module); + _tokenManager.EnsureDefTokensAreAvailable(tse, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); break; default: // We don't need to worry about string handles @@ -815,12 +821,12 @@ void EnsureAsyncThunkTokensAreAvailable(MethodDesc method) if (region.Kind == ILExceptionRegionKind.Catch) { TypeSystemEntity catchType = (TypeSystemEntity)il.GetObject(region.ClassToken); - _tokenManager.EnsureDefTokensAreAvailable(catchType, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module); + _tokenManager.EnsureDefTokensAreAvailable(catchType, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); } } foreach (var local in il.GetLocals()) { - _tokenManager.EnsureDefTokensAreAvailable(local.Type, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module); + _tokenManager.EnsureDefTokensAreAvailable(local.Type, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); } } finally @@ -1013,9 +1019,9 @@ private void EnsureInstantiationReferencesArePresentForExternalMethod(MethodDesc // have tokens. var moduleForNewReferences = ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module; foreach (var type in method.Instantiation) - _tokenManager.EnsureDefTokensAreAvailable(type, moduleForNewReferences); + _tokenManager.EnsureDefTokensAreAvailable(type, moduleForNewReferences, false); foreach (var type in method.OwningType.Instantiation) - _tokenManager.EnsureDefTokensAreAvailable(type, moduleForNewReferences); + _tokenManager.EnsureDefTokensAreAvailable(type, moduleForNewReferences, false); } private void AddNecessaryAsyncReferences(MethodDesc method) @@ -1051,7 +1057,7 @@ private void AddNecessaryAsyncReferences(MethodDesc method) asyncHelpers.GetKnownMethod("AllocContinuationMethod"u8, null), ]; var moduleForNewReferences = ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module; - _tokenManager.EnsureDefTokensAreAvailable([..requiredMethods, ..requiredTypes, ..requiredFields], moduleForNewReferences); + _tokenManager.EnsureDefTokensAreAvailable([..requiredMethods, ..requiredTypes, ..requiredFields], moduleForNewReferences, true); _hasAddedAsyncReferences = true; } From c66c646114b88c1a78a902415c8f89c2545a30ad Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:25:59 -0700 Subject: [PATCH 45/74] Properly get the EcmaMethod in CreateCrossModuleInlineableTokensForILBody --- .../tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs index 448b25e7cae86c..961c634100caa5 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs @@ -153,7 +153,7 @@ public void CreateCrossModuleInlineableTokensForILBody(MethodDesc method) Debug.Assert(!_compilationModuleGroup.VersionsWithMethodBody(method) && _compilationModuleGroup.CrossModuleInlineable(method)); - if (!wrappedMethodIL.Initialize(_manifestMutableModule, EcmaMethodIL.Create((EcmaMethod)method))) + if (!wrappedMethodIL.Initialize(_manifestMutableModule, EcmaMethodIL.Create((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()))) { // If we could not initialize the wrapped method IL, we should store a null. // That will result in the IL code for the method being unavailable for use in From 303dcdc13d2a0fdafee2628185c095651f911efc Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:45:55 -0700 Subject: [PATCH 46/74] Get IL for the (possible instantiated) method, not the definition --- .../Compiler/ReadyToRunCodegenCompilation.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs index ed1b1698bc0e0a..743e58e2fba29a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs @@ -786,9 +786,7 @@ void EnsureAsyncThunkTokensAreAvailable(MethodDesc method) return; } var ilProvider = (ReadyToRunILProvider)_methodILCache.ILProvider; - MethodIL il = null!; - var methodDef = method.GetTypicalMethodDefinition(); - il = ilProvider.GetMethodIL(methodDef); + MethodIL il = ilProvider.GetMethodIL(method); // We shouldn't get null IL, but just in case, handle it gracefully Debug.Assert(il is not null); if (il is null) @@ -805,7 +803,7 @@ void EnsureAsyncThunkTokensAreAvailable(MethodDesc method) switch(il.GetObject(tok)) { case TypeSystemEntity tse: - _tokenManager.EnsureDefTokensAreAvailable(tse, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); + _tokenManager.EnsureDefTokensAreAvailable(tse, ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); break; default: // We don't need to worry about string handles @@ -821,12 +819,12 @@ void EnsureAsyncThunkTokensAreAvailable(MethodDesc method) if (region.Kind == ILExceptionRegionKind.Catch) { TypeSystemEntity catchType = (TypeSystemEntity)il.GetObject(region.ClassToken); - _tokenManager.EnsureDefTokensAreAvailable(catchType, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); + _tokenManager.EnsureDefTokensAreAvailable(catchType, ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); } } foreach (var local in il.GetLocals()) { - _tokenManager.EnsureDefTokensAreAvailable(local.Type, ((EcmaMethod)methodDef.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); + _tokenManager.EnsureDefTokensAreAvailable(local.Type, ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); } } finally From 0ef7dfc03bc99452f7be168d4e599477b1d8ffd4 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:50:36 -0700 Subject: [PATCH 47/74] Expand R2R composite-async test coverage and refactor R2RAssert to bool helpers - Refactor R2RAssert.HasX helpers in R2RResultChecker.cs to return bool with an out-string diagnostic, asserted at the call site as Assert.True/False. Each Validate body declares a single shared 'string diag;' and reuses it. Allows negative assertions (Assert.False(R2RAssert.HasX(...))) which were not expressible with the prior Assert.Fail-throwing form. HasCrossModuleInliners now takes IEnumerable so callers can use collection expressions. - Add CompositeAsyncContinuationAndResume: composite-mode runtime- async with GC refs captured across awaits, validating async variants, RESUME stubs, and ContinuationLayout fixups. - Add CompositeAsyncInliningMatrix: 6-case matrix (Task / Task / Task x with-await / no-await) validating that awaitless async candidates inline in composite mode while async-with-await methods do not (JIT FATAL CALLEE_AWAIT in importercalls.cpp). - Add CompositeDoesNotProduceCrossModuleInliningInfo: negative test confirming composite mode emits per-module InliningInfo2 rather than CrossModuleInlineInfo when all inlinees are composite inputs. - Fix MultiStepCompositeAndNonCompositeAsync wiring: consumer source referenced AsyncDepLibContinuation but the test wired AsyncDepLib. - Remove [ActiveIssue] markers from five tests now passing thanks to the parent PR enabling async thunks in composite mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CrossModuleInlining/AsyncInlineCallers.cs | 44 +++ .../Dependencies/AsyncInlineCandidatesLib.cs | 46 +++ .../TestCases/R2RTestSuites.cs | 312 +++++++++++++----- .../CompositeAsyncContinuationMain.cs | 30 ++ .../Dependencies/AsyncCompositeContLib.cs | 28 ++ .../TestCasesRunner/R2RResultChecker.cs | 224 +++++++------ 6 files changed, 511 insertions(+), 173 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs new file mode 100644 index 00000000000000..da1e0dff49d4f8 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs @@ -0,0 +1,44 @@ +// Six call sites that each invoke one of the AsyncInlineCandidatesLib methods. +// Each caller is marked NoInlining so it stays put as the inliner; the test +// then asserts which candidates get inlined into their respective callers. +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncInlineCallers +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallReturnTaskNoAwait() + { + await AsyncInlineCandidatesLib.ReturnTaskNoAwait(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallReturnTaskPrimitiveNoAwait() + { + return await AsyncInlineCandidatesLib.ReturnTaskPrimitiveNoAwait(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallReturnTaskClassNoAwait() + { + return await AsyncInlineCandidatesLib.ReturnTaskClassNoAwait(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallReturnTaskWithAwait() + { + await AsyncInlineCandidatesLib.ReturnTaskWithAwait(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallReturnTaskPrimitiveWithAwait() + { + return await AsyncInlineCandidatesLib.ReturnTaskPrimitiveWithAwait(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallReturnTaskClassWithAwait() + { + return await AsyncInlineCandidatesLib.ReturnTaskClassWithAwait(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs new file mode 100644 index 00000000000000..d4d40a11218fe4 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs @@ -0,0 +1,46 @@ +// Dependency library exposing six runtime-async inlining candidates that cross +// the {Task, Task, Task} x {with-await, without-await} matrix. +// The JIT cannot inline an async method that performs an actual await +// (Compiler::impSetupAsyncCall reports CALLEE_AWAIT FATAL in importercalls.cpp; +// AsyncSuspend reports CALLEE_ASYNC_SUSPEND FATAL). Methods without an await +// are eligible for inlining like any other small method. +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncInlineCandidatesLib +{ + // --- Awaitless variants: should be inlinable --- + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task ReturnTaskNoAwait() + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task ReturnTaskPrimitiveNoAwait() => 42; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task ReturnTaskClassNoAwait() => "no_await"; + + // --- Variants containing an actual await: cannot be inlined by the JIT --- + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task ReturnTaskWithAwait() + { + await Task.Yield(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task ReturnTaskPrimitiveWithAwait() + { + await Task.Yield(); + return 42; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task ReturnTaskClassWithAwait() + { + await Task.Yield(); + return "with_await"; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 9e42292047dfe9..2196d996705e53 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; using ILCompiler.Reflection.ReadyToRun; -using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; @@ -49,10 +48,11 @@ [new CrossgenCompilation(basicCrossModuleInlining.AssemblyName, [cgInlineableLib static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "InlineableLib"); - R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetValue", "GetValue"); - R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetString", "GetString"); - R2RAssert.HasCrossModuleInliningInfo(reader); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLib", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetValue", "GetValue", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetString", "GetString", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInliningInfo(reader, out diag), diag); } } @@ -90,9 +90,10 @@ public void TransitiveReferences() { Validate = reader => { - R2RAssert.HasManifestRef(reader, "InlineableLibTransitive"); - R2RAssert.HasManifestRef(reader, "ExternalLib"); - R2RAssert.HasCrossModuleInlinedMethod(reader, "TestTransitiveValue", "GetExternalValue"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLibTransitive", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "ExternalLib", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestTransitiveValue", "GetExternalValue", out diag), diag); }, }, ])); @@ -132,8 +133,9 @@ public void AsyncCrossModuleInlining() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "AsyncInlineableLib"); - R2RAssert.HasCrossModuleInlinedMethod(reader, "TestAsyncInline", "GetValueAsync"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncInlineableLib", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestAsyncInline", "GetValueAsync", out diag), diag); } } @@ -168,7 +170,8 @@ public void CompositeBasic() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "CompositeLib"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "CompositeLib", out diag), diag); } } @@ -197,9 +200,10 @@ public void RuntimeAsyncMethodEmission() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasAsyncVariant(reader, "SimpleAsyncMethod"); - R2RAssert.HasAsyncVariant(reader, "AsyncVoidReturn"); - R2RAssert.HasAsyncVariant(reader, "ValueTaskMethod"); + string diag; + Assert.True(R2RAssert.HasAsyncVariant(reader, "SimpleAsyncMethod", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "AsyncVoidReturn", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "ValueTaskMethod", out diag), diag); } } @@ -233,11 +237,12 @@ public void RuntimeAsyncContinuationLayout() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasAsyncVariant(reader, "CaptureObjectAcrossAwait"); - R2RAssert.HasAsyncVariant(reader, "CaptureMultipleRefsAcrossAwait"); - R2RAssert.HasContinuationLayout(reader, "CaptureObjectAcrossAwait"); - R2RAssert.HasContinuationLayout(reader, "CaptureMultipleRefsAcrossAwait"); - R2RAssert.HasResumptionStubFixup(reader, "CaptureObjectAcrossAwait"); + string diag; + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureObjectAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureMultipleRefsAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureObjectAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureMultipleRefsAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasResumptionStubFixup(reader, "CaptureObjectAcrossAwait", out diag), diag); } } @@ -270,7 +275,8 @@ public void RuntimeAsyncDevirtualize() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasAsyncVariant(reader, "GetValueAsync"); + string diag; + Assert.True(R2RAssert.HasAsyncVariant(reader, "GetValueAsync", out diag), diag); } } @@ -303,8 +309,9 @@ public void RuntimeAsyncNoYield() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasAsyncVariant(reader, "AsyncButNoAwait"); - R2RAssert.HasAsyncVariant(reader, "AsyncWithConditionalAwait"); + string diag; + Assert.True(R2RAssert.HasAsyncVariant(reader, "AsyncButNoAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "AsyncWithConditionalAwait", out diag), diag); } } @@ -356,8 +363,9 @@ public void RuntimeAsyncCrossModule() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "AsyncDepLib"); - R2RAssert.HasAsyncVariant(reader, "CallCrossModuleAsync"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLib", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleAsync", out diag), diag); } } @@ -401,8 +409,60 @@ public void CompositeCrossModuleInlining() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "InlineableLib"); - R2RAssert.HasInlinedMethod(reader, "TestGetValue", "GetValue"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLib", out diag), diag); + Assert.True(R2RAssert.HasInlinedMethod(reader, "TestGetValue", "GetValue", out diag), diag); + } + } + + /// + /// Negative test: composite mode does NOT produce a CrossModuleInlineInfo section. + /// True cross-module inlining (recorded in CrossModuleInlineInfo) requires the inlinee + /// to live in a module OUTSIDE the version bubble (added via --opt-cross-module on a + /// reference assembly). In composite mode, all input modules are part of the same + /// version bubble, so cross-module inlining info is never produced — inlining between + /// composite inputs is recorded in the per-module InliningInfo2 section instead. + /// Compare with , which uses the same source modules + /// in a non-composite layout and DOES produce CrossModuleInlineInfo entries. + /// + [Fact] + public void CompositeDoesNotProduceCrossModuleInliningInfo() + { + var inlineableLib = new CompiledAssembly + { + AssemblyName = "InlineableLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], + }; + var compositeMain = new CompiledAssembly + { + AssemblyName = nameof(CompositeDoesNotProduceCrossModuleInliningInfo), + SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], + References = [inlineableLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeDoesNotProduceCrossModuleInliningInfo), + [ + new(nameof(CompositeDoesNotProduceCrossModuleInliningInfo), + [ + new CrossgenAssembly(inlineableLib), + new CrossgenAssembly(compositeMain), + ]) + { + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + string diag; + // Inlining still happens in composite mode — recorded in InliningInfo2 — confirming + // the assertion below is meaningful (we are not just looking at a no-op compilation). + Assert.True(R2RAssert.HasInlinedMethod(reader, "TestGetValue", "GetValue", out diag), diag); + + // But no CrossModuleInlineInfo section/entries should be present in composite output. + Assert.False(R2RAssert.HasCrossModuleInliningInfo(reader, out diag), diag); } } @@ -410,7 +470,6 @@ static void Validate(ReadyToRunReader reader) /// Composite mode with runtime-async methods in both assemblies. /// Validates async variants exist in composite output. /// - [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] [Fact] public void CompositeAsync() { @@ -444,9 +503,10 @@ public void CompositeAsync() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); - R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); - R2RAssert.HasAsyncVariant(reader, "GetValueAsync"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncCompositeLib", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "GetValueAsync", out diag), diag); } } @@ -456,31 +516,103 @@ static void Validate(ReadyToRunReader reader) /// within a composite image, exercising MutableModule token encoding for /// cross-module async continuation layouts. /// - [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] + /// + /// Composite + runtime-async + intra-bubble inlining matrix test. + /// Verifies that, in composite mode, awaitless async candidates ARE inlined into + /// their callers (recorded in InliningInfo2 — composite inputs share a version + /// bubble, so true CrossModuleInlineInfo entries are not produced for them), while + /// candidates whose body contains a real await are NOT inlined. The latter + /// is a JIT-level limitation: Compiler::impSetupAsyncCall in + /// importercalls.cpp issues a FATAL CALLEE_AWAIT observation as soon as it + /// sees an async call inside an inlining candidate (see also CALLEE_ASYNC_SUSPEND). + /// The matrix covers Task, Task<primitive>, and Task<class> return shapes. + /// [Fact] - public void CompositeAsyncCrossModuleInlining() + public void CompositeAsyncInliningMatrix() { - var asyncCompositeLib = new CompiledAssembly + var asyncInlineCandidatesLib = new CompiledAssembly { - AssemblyName = "AsyncCompositeLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], + AssemblyName = "AsyncInlineCandidatesLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs"], Features = { RuntimeAsyncFeature }, }; - var compositeAsyncMain = new CompiledAssembly + var asyncInlineCallers = new CompiledAssembly { - AssemblyName = "CompositeAsyncMain", - SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], + AssemblyName = "AsyncInlineCallers", + SourceResourceNames = ["CrossModuleInlining/AsyncInlineCallers.cs"], Features = { RuntimeAsyncFeature }, - References = [asyncCompositeLib] + References = [asyncInlineCandidatesLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(CompositeAsyncCrossModuleInlining), + nameof(CompositeAsyncInliningMatrix), [ - new(nameof(CompositeAsyncCrossModuleInlining), + new(nameof(CompositeAsyncInliningMatrix), [ - new CrossgenAssembly(asyncCompositeLib), - new CrossgenAssembly(compositeAsyncMain), + new CrossgenAssembly(asyncInlineCandidatesLib), + new CrossgenAssembly(asyncInlineCallers), + ]) + { + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncInlineCandidatesLib", out diag), diag); + + // Awaitless async candidates: should be inlined into their callers. + Assert.True(R2RAssert.HasInlinedMethod(reader, "CallReturnTaskNoAwait", "ReturnTaskNoAwait", out diag), diag); + Assert.True(R2RAssert.HasInlinedMethod(reader, "CallReturnTaskPrimitiveNoAwait", "ReturnTaskPrimitiveNoAwait", out diag), diag); + Assert.True(R2RAssert.HasInlinedMethod(reader, "CallReturnTaskClassNoAwait", "ReturnTaskClassNoAwait", out diag), diag); + + // Async candidates that contain a real await: cannot be inlined by the JIT. + Assert.False(R2RAssert.HasInlinedMethod(reader, "CallReturnTaskWithAwait", "ReturnTaskWithAwait", out diag), diag); + Assert.False(R2RAssert.HasInlinedMethod(reader, "CallReturnTaskPrimitiveWithAwait", "ReturnTaskPrimitiveWithAwait", out diag), diag); + Assert.False(R2RAssert.HasInlinedMethod(reader, "CallReturnTaskClassWithAwait", "ReturnTaskClassWithAwait", out diag), diag); + } + } + + /// + /// Composite mode with runtime-async methods that capture GC refs across await + /// points, exercising ContinuationLayout and RESUME stub emission in composite images. + /// Validates that both the library and main assembly's async methods produce + /// [ASYNC] variants, [RESUME] stubs, and ContinuationLayout fixups. + /// + [Fact] + public void CompositeAsyncContinuationAndResume() + { + var asyncCompositeContLib = new CompiledAssembly + { + AssemblyName = "AsyncCompositeContLib", + SourceResourceNames = + [ + "RuntimeAsync/Dependencies/AsyncCompositeContLib.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + }; + var compositeAsyncContMain = new CompiledAssembly + { + AssemblyName = "CompositeAsyncContinuationMain", + SourceResourceNames = + [ + "RuntimeAsync/CompositeAsyncContinuationMain.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + References = [asyncCompositeContLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeAsyncContinuationAndResume), + [ + new(nameof(CompositeAsyncContinuationAndResume), + [ + new CrossgenAssembly(asyncCompositeContLib), + new CrossgenAssembly(compositeAsyncContMain), ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], @@ -490,10 +622,22 @@ public void CompositeAsyncCrossModuleInlining() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); - R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); - R2RAssert.HasInlinedMethod(reader, "CallCompositeAsync", "GetValueAsync"); - R2RAssert.HasContinuationLayout(reader, "CallCompositeAsync"); + string diag; + // Library methods produce async variants and resume stubs + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefComposite", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureArrayComposite", out diag), diag); + Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureRefComposite", out diag), diag); + Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureArrayComposite", out diag), diag); + + // Main assembly methods produce async variants and resume stubs + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureRefComposite", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "LocalCaptureAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasResumptionStub(reader, "CallCaptureRefComposite", out diag), diag); + Assert.True(R2RAssert.HasResumptionStub(reader, "LocalCaptureAcrossAwait", out diag), diag); + + // ContinuationLayout fixups are present for methods with GC refs across awaits + Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureRefComposite", out diag), diag); + Assert.True(R2RAssert.HasContinuationLayout(reader, "LocalCaptureAcrossAwait", out diag), diag); } } @@ -546,9 +690,10 @@ public void AsyncCrossModuleContinuation() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation"); - R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef"); - R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureArray"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureArray", out diag), diag); } } @@ -589,7 +734,8 @@ public void MultiStepCompositeAndNonComposite() Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = reader => { - R2RAssert.HasManifestRef(reader, "MultiStepLibA"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "MultiStepLibA", out diag), diag); }, }, new("NonCompositeStep", @@ -604,8 +750,9 @@ public void MultiStepCompositeAndNonComposite() { Validate = reader => { - R2RAssert.HasManifestRef(reader, "MultiStepLibA"); - R2RAssert.HasCrossModuleInlinedMethod(reader, "GetValueFromLibA", "GetValue"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "MultiStepLibA", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "GetValueFromLibA", "GetValue", out diag), diag); }, }, ])); @@ -619,7 +766,6 @@ public void MultiStepCompositeAndNonComposite() /// Composite + runtime-async + cross-module devirtualization. /// Interface defined in AsyncInterfaceLib, call sites in CompositeAsyncDevirtMain. /// - [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] [Fact] public void CompositeAsyncDevirtualize() { @@ -657,8 +803,9 @@ public void CompositeAsyncDevirtualize() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "AsyncInterfaceLib"); - R2RAssert.HasAsyncVariant(reader, "CallOnSealed"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncInterfaceLib", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallOnSealed", out diag), diag); } } @@ -704,8 +851,9 @@ public void CompositeTransitive() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "InlineableLibTransitive"); - R2RAssert.HasManifestRef(reader, "ExternalLib"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLibTransitive", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "ExternalLib", out diag), diag); } } @@ -764,8 +912,9 @@ public void AsyncCrossModuleTransitive() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib"); - R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync", out diag), diag); } } @@ -773,7 +922,6 @@ static void Validate(ReadyToRunReader reader) /// Composite + runtime-async + transitive (3 assemblies). /// Full combination of composite, async, and transitive references. /// - [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] [Fact] public void CompositeAsyncTransitive() { @@ -822,8 +970,9 @@ public void CompositeAsyncTransitive() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib"); - R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync", out diag), diag); } } @@ -832,22 +981,20 @@ static void Validate(ReadyToRunReader reader) /// Step 1: Composite of async libs. Step 2: Non-composite consumer /// with cross-module inlining of async methods. /// - [ActiveIssue("https://github.com/dotnet/runtime/issues/125337")] [Fact] public void MultiStepCompositeAndNonCompositeAsync() { - var asyncCompositeLib = new CompiledAssembly + var asyncDepLib = new CompiledAssembly { - AssemblyName = "AsyncCompositeLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], + AssemblyName = "AsyncDepLibContinuation", + SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs"], Features = { RuntimeAsyncFeature }, }; - var compositeAsyncMain = new CompiledAssembly + var asyncCompositeLib = new CompiledAssembly { - AssemblyName = "CompositeAsyncMain", - SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], + AssemblyName = "AsyncCompositeLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], Features = { RuntimeAsyncFeature }, - References = [asyncCompositeLib] }; var asyncConsumer = new CompiledAssembly { @@ -858,7 +1005,7 @@ public void MultiStepCompositeAndNonCompositeAsync() "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [asyncCompositeLib] + References = [asyncDepLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -866,21 +1013,22 @@ public void MultiStepCompositeAndNonCompositeAsync() [ new("CompositeAsyncStep", [ + new CrossgenAssembly(asyncDepLib), new CrossgenAssembly(asyncCompositeLib), - new CrossgenAssembly(compositeAsyncMain), ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = reader => { - R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); - R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefAcrossAwait", out diag), diag); }, }, new("NonCompositeAsyncStep", [ new CrossgenAssembly(asyncConsumer), - new CrossgenAssembly(asyncCompositeLib) + new CrossgenAssembly(asyncDepLib) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -889,8 +1037,9 @@ public void MultiStepCompositeAndNonCompositeAsync() { Validate = reader => { - R2RAssert.HasManifestRef(reader, "AsyncCompositeLib"); - R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef"); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef", out diag), diag); }, }, ])); @@ -937,14 +1086,15 @@ public void CrossModuleGenericMultiInliner() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasManifestRef(reader, "CrossModuleGenericLib"); - R2RAssert.HasCrossModuleInliningInfo(reader); + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "CrossModuleGenericLib", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInliningInfo(reader, out diag), diag); // Verify that GetValue has cross-module inliners from both GenericWrapperA and GenericWrapperB. // This exercises the cross-module inliner parsing path where indices // must be read as absolute values, not delta-accumulated, and validates // that the resolved method names match the expected inliners. - R2RAssert.HasCrossModuleInliners(reader, "GetValue", "GenericWrapperA", "GenericWrapperB"); + Assert.True(R2RAssert.HasCrossModuleInliners(reader, "GetValue", ["GenericWrapperA", "GenericWrapperB"], out diag), diag); } } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs new file mode 100644 index 00000000000000..2419d72ac82af4 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs @@ -0,0 +1,30 @@ +// Test: Composite mode async with continuation layouts and resumption stubs. +// Calls async methods from AsyncCompositeContLib that capture GC refs across +// await points, exercising composite-mode ContinuationLayout and RESUME stub emission. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class CompositeAsyncContinuationMain +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCaptureRefComposite() + { + return await AsyncCompositeContLib.CaptureRefComposite(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCaptureArrayComposite() + { + return await AsyncCompositeContLib.CaptureArrayComposite(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task LocalCaptureAcrossAwait() + { + object o = new object(); + string s = "local"; + await Task.Yield(); + return s.Length + o.GetHashCode(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs new file mode 100644 index 00000000000000..85ff77178e7502 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs @@ -0,0 +1,28 @@ +// Dependency library for composite async continuation tests. +// Contains runtime-async methods that capture GC refs across await points. +// Used in composite mode to exercise MutableModule token encoding for +// cross-module async continuation layouts and resumption stubs. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncCompositeContLib +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CaptureRefComposite() + { + object o = new object(); + string s = "composite_ref"; + await Task.Yield(); + return s + o.GetHashCode(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CaptureArrayComposite() + { + int[] arr = new int[] { 5, 10, 15 }; + string label = "total"; + await Task.Yield(); + return arr[0] + arr[1] + label.Length; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index e3e47b450fd9b1..e27f50adf10338 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -35,9 +35,9 @@ public static List GetAllMethods(ReadyToRunReader reader) } /// - /// Asserts the R2R image contains a manifest or MSIL assembly reference with the given name. + /// Returns true if the R2R image contains a manifest or MSIL assembly reference with the given name. /// - public static void HasManifestRef(ReadyToRunReader reader, string assemblyName) + public static bool HasManifestRef(ReadyToRunReader reader, string assemblyName, out string diagnostic) { var allRefs = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -55,19 +55,24 @@ public static void HasManifestRef(ReadyToRunReader reader, string assemblyName) foreach (var kvp in reader.ManifestReferenceAssemblies) allRefs.Add(kvp.Key); - Assert.True(allRefs.Contains(assemblyName), - $"Expected assembly reference '{assemblyName}' not found. " + - $"Found: [{string.Join(", ", allRefs.OrderBy(s => s))}]"); + bool found = allRefs.Contains(assemblyName); + diagnostic = found + ? $"Found manifest/MSIL ref '{assemblyName}'." + : $"Expected assembly reference '{assemblyName}' not found. " + + $"Found: [{string.Join(", ", allRefs.OrderBy(s => s))}]"; + return found; } /// - /// Asserts that the CrossModuleInlineInfo section records that - /// inlined , and that the inlinee is encoded as a cross-module - /// reference (ILBody import index, not a local MethodDef RID). + /// Returns true if the CrossModuleInlineInfo section records that + /// inlined and the inlinee is encoded as a cross-module + /// reference (ILBody import index, not a local MethodDef RID). If a pair matches but the + /// encoding is wrong, returns false with the mismatch described in . /// - public static void HasCrossModuleInlinedMethod(ReadyToRunReader reader, string inlinerMethodName, string inlineeMethodName) + public static bool HasCrossModuleInlinedMethod(ReadyToRunReader reader, string inlinerMethodName, string inlineeMethodName, out string diagnostic) { - var inliningInfo = GetCrossModuleInliningInfoSection(reader); + if (!TryGetCrossModuleInliningInfoSection(reader, out var inliningInfo, out diagnostic)) + return false; var allPairs = new List(); foreach (var (inlinerName, inlineeName, inlineeKind) in inliningInfo.GetInliningPairs()) @@ -77,31 +82,40 @@ public static void HasCrossModuleInlinedMethod(ReadyToRunReader reader, string i if (inlinerName.Contains(inlinerMethodName, StringComparison.OrdinalIgnoreCase) && inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) { - Assert.True(inlineeKind == CrossModuleInliningInfoSection.InlineeReferenceKind.CrossModule, + if (inlineeKind == CrossModuleInliningInfoSection.InlineeReferenceKind.CrossModule) + { + diagnostic = $"Found cross-module inlining '{inlinerName} → {inlineeName}'."; + return true; + } + + diagnostic = $"Found inlining pair '{inlinerName} → {inlineeName}' but the inlinee is not encoded " + - $"as a cross-module reference ({inlineeKind}). Expected ILBody import encoding."); - return; + $"as a cross-module reference ({inlineeKind}). Expected ILBody import encoding."; + return false; } } - Assert.Fail( + diagnostic = $"Expected cross-module inlining '{inlineeMethodName}' into '{inlinerMethodName}', but it was not found.\n" + - $"Recorded inlining pairs:\n {string.Join("\n ", allPairs)}"); + $"Recorded inlining pairs:\n {string.Join("\n ", allPairs)}"; + return false; } /// - /// Asserts that the CrossModuleInlineInfo section has an entry for an inlinee matching + /// Returns true if the CrossModuleInlineInfo section has an entry for an inlinee matching /// with cross-module inliners whose resolved names /// contain each of the . This validates that /// cross-module inliner indices (encoded as absolute ILBody import indices) resolve /// to the correct method names. /// - public static void HasCrossModuleInliners( + public static bool HasCrossModuleInliners( ReadyToRunReader reader, string inlineeMethodName, - params string[] expectedInlinerNames) + IEnumerable expectedInlinerNames, + out string diagnostic) { - var inliningInfo = GetCrossModuleInliningInfoSection(reader); + if (!TryGetCrossModuleInliningInfoSection(reader, out var inliningInfo, out diagnostic)) + return false; foreach (var entry in inliningInfo.GetEntries()) { @@ -118,30 +132,37 @@ public static void HasCrossModuleInliners( foreach (string expected in expectedInlinerNames) { - Assert.True( - crossModuleInlinerNames.Any(n => n.Contains(expected, StringComparison.OrdinalIgnoreCase)), - $"Inlinee '{inlineeName}': expected a cross-module inliner matching '{expected}' " + - $"but found only:\n {string.Join("\n ", crossModuleInlinerNames)}"); + if (!crossModuleInlinerNames.Any(n => n.Contains(expected, StringComparison.OrdinalIgnoreCase))) + { + diagnostic = + $"Inlinee '{inlineeName}': expected a cross-module inliner matching '{expected}' " + + $"but found only:\n {string.Join("\n ", crossModuleInlinerNames)}"; + return false; + } } - return; + diagnostic = + $"Inlinee '{inlineeName}' has all expected cross-module inliners: " + + $"[{string.Join(", ", expectedInlinerNames)}]"; + return true; } var allEntries = new List(); foreach (var (inlinerName, inlineeName, _) in inliningInfo.GetInliningPairs()) allEntries.Add($"{inlinerName} → {inlineeName}"); - Assert.Fail( + diagnostic = $"No CrossModuleInlineInfo entry found for inlinee matching '{inlineeMethodName}'.\n" + - $"All inlining pairs:\n {string.Join("\n ", allEntries)}"); + $"All inlining pairs:\n {string.Join("\n ", allEntries)}"; + return false; } /// - /// Asserts that any inlining info section (CrossModuleInlineInfo or InliningInfo2) records that - /// inlined . + /// Returns true if any inlining info section (CrossModuleInlineInfo or InliningInfo2) records + /// that inlined . /// Does not check whether the encoding is cross-module or local. /// - public static void HasInlinedMethod(ReadyToRunReader reader, string inlinerMethodName, string inlineeMethodName) + public static bool HasInlinedMethod(ReadyToRunReader reader, string inlinerMethodName, string inlineeMethodName, out string diagnostic) { var foundPairs = new List(); @@ -153,7 +174,8 @@ public static void HasInlinedMethod(ReadyToRunReader reader, string inlinerMetho if (inlinerName.Contains(inlinerMethodName, StringComparison.OrdinalIgnoreCase) && inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) { - return; + diagnostic = $"Found inlining '{inlinerName} -> {inlineeName}' in CrossModuleInlineInfo."; + return true; } foundPairs.Add($"[CXMI] {inlinerName} -> {inlineeName}"); } @@ -166,19 +188,18 @@ public static void HasInlinedMethod(ReadyToRunReader reader, string inlinerMetho if (inlinerName.Contains(inlinerMethodName, StringComparison.OrdinalIgnoreCase) && inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) { - return; + diagnostic = $"Found inlining '{inlinerName} -> {inlineeName}' in InliningInfo2."; + return true; } foundPairs.Add($"[II2] {inlinerName} -> {inlineeName}"); } } - string pairList = foundPairs.Count > 0 - ? string.Join("\n ", foundPairs) - : "(none)"; - - Assert.Fail( - $"Expected inlining '{inlineeMethodName}' into '{inlinerMethodName}', but it was not found.\n" + - $"Found inlining pairs:\n {pairList}"); + string pairList = foundPairs.Count > 0 ? string.Join("\n ", foundPairs) : "(none)"; + diagnostic = + $"Inlining '{inlineeMethodName}' into '{inlinerMethodName}' not found.\n" + + $"Inlining pairs in image:\n {pairList}"; + return false; } private static CrossModuleInliningInfoSection GetCrossModuleInliningInfoSection(ReadyToRunReader reader) @@ -194,6 +215,23 @@ private static CrossModuleInliningInfoSection GetCrossModuleInliningInfoSection( return new CrossModuleInliningInfoSection(reader, offset, endOffset); } + private static bool TryGetCrossModuleInliningInfoSection(ReadyToRunReader reader, out CrossModuleInliningInfoSection inliningInfo, out string diagnostic) + { + if (!reader.ReadyToRunHeader.Sections.TryGetValue( + ReadyToRunSectionType.CrossModuleInlineInfo, out ReadyToRunSection section)) + { + inliningInfo = null!; + diagnostic = "Expected CrossModuleInlineInfo section not found in R2R image."; + return false; + } + + int offset = reader.GetOffset(section.RelativeVirtualAddress); + int endOffset = offset + section.Size; + inliningInfo = new CrossModuleInliningInfoSection(reader, offset, endOffset); + diagnostic = ""; + return true; + } + private static IEnumerable GetAllInliningInfo2Sections(ReadyToRunReader reader) { // InliningInfo2 can appear in the global header @@ -226,95 +264,90 @@ private static IEnumerable GetAllInliningInfo2Sections(Rea } /// - /// Asserts the R2R image contains a CrossModuleInlineInfo section with at least one entry. + /// Returns true if the R2R image contains a CrossModuleInlineInfo section with at least one entry. /// - public static void HasCrossModuleInliningInfo(ReadyToRunReader reader) + public static bool HasCrossModuleInliningInfo(ReadyToRunReader reader, out string diagnostic) { - Assert.True( - reader.ReadyToRunHeader.Sections.TryGetValue( - ReadyToRunSectionType.CrossModuleInlineInfo, out ReadyToRunSection section), - "Expected CrossModuleInlineInfo section not found in R2R image."); + if (!TryGetCrossModuleInliningInfoSection(reader, out var inliningInfo, out diagnostic)) + return false; - int offset = reader.GetOffset(section.RelativeVirtualAddress); - int endOffset = offset + section.Size; - var inliningInfo = new CrossModuleInliningInfoSection(reader, offset, endOffset); string dump = inliningInfo.ToString(); + if (dump.Length == 0) + { + diagnostic = "CrossModuleInlineInfo section is present but contains no entries."; + return false; + } - Assert.True( - dump.Length > 0, - "CrossModuleInlineInfo section is present but contains no entries."); + diagnostic = $"CrossModuleInlineInfo contains entries:\n{dump}"; + return true; } /// - /// Asserts the R2R image contains an [ASYNC] variant entry whose signature contains the given method name. + /// Returns true if the R2R image contains an [ASYNC] variant entry whose signature contains the given method name. /// - public static void HasAsyncVariant(ReadyToRunReader reader, string methodName) + public static bool HasAsyncVariant(ReadyToRunReader reader, string methodName, out string diagnostic) { var asyncSigs = GetAllMethods(reader) .Where(m => m.SignatureString.Contains("[ASYNC]", StringComparison.OrdinalIgnoreCase)) .Select(m => m.SignatureString) .ToList(); - Assert.True( - asyncSigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)), - $"Expected [ASYNC] variant for '{methodName}' not found. " + - $"Async methods: [{string.Join(", ", asyncSigs)}]"); + bool found = asyncSigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)); + diagnostic = found + ? $"Found [ASYNC] variant for '{methodName}'." + : $"Expected [ASYNC] variant for '{methodName}' not found. " + + $"Async methods: [{string.Join(", ", asyncSigs)}]"; + return found; } /// - /// Asserts the R2R image contains a [RESUME] stub entry whose signature contains the given method name. + /// Returns true if the R2R image contains a [RESUME] stub entry whose signature contains the given method name. /// - public static void HasResumptionStub(ReadyToRunReader reader, string methodName) + public static bool HasResumptionStub(ReadyToRunReader reader, string methodName, out string diagnostic) { var resumeSigs = GetAllMethods(reader) .Where(m => m.SignatureString.Contains("[RESUME]", StringComparison.OrdinalIgnoreCase)) .Select(m => m.SignatureString) .ToList(); - Assert.True( - resumeSigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)), - $"Expected [RESUME] stub for '{methodName}' not found. " + - $"Resume methods: [{string.Join(", ", resumeSigs)}]"); + bool found = resumeSigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)); + diagnostic = found + ? $"Found [RESUME] stub for '{methodName}'." + : $"Expected [RESUME] stub for '{methodName}' not found. " + + $"Resume methods: [{string.Join(", ", resumeSigs)}]"; + return found; } /// - /// Asserts the R2R image contains at least one ContinuationLayout fixup. + /// Returns true if the R2R image contains at least one ContinuationLayout fixup. /// - public static void HasContinuationLayout(ReadyToRunReader reader) - { - HasFixupKind(reader, ReadyToRunFixupKind.ContinuationLayout); - } + public static bool HasContinuationLayout(ReadyToRunReader reader, out string diagnostic) + => HasFixupKind(reader, ReadyToRunFixupKind.ContinuationLayout, out diagnostic); /// - /// Asserts a method whose signature contains + /// Returns true if a method whose signature contains /// has at least one ContinuationLayout fixup. /// - public static void HasContinuationLayout(ReadyToRunReader reader, string methodName) - { - HasFixupKindOnMethod(reader, ReadyToRunFixupKind.ContinuationLayout, methodName); - } + public static bool HasContinuationLayout(ReadyToRunReader reader, string methodName, out string diagnostic) + => HasFixupKindOnMethod(reader, ReadyToRunFixupKind.ContinuationLayout, methodName, out diagnostic); /// - /// Asserts the R2R image contains at least one ResumptionStubEntryPoint fixup. + /// Returns true if the R2R image contains at least one ResumptionStubEntryPoint fixup. /// - public static void HasResumptionStubFixup(ReadyToRunReader reader) - { - HasFixupKind(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint); - } + public static bool HasResumptionStubFixup(ReadyToRunReader reader, out string diagnostic) + => HasFixupKind(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint, out diagnostic); /// - /// Asserts a method whose signature contains + /// Returns true if a method whose signature contains /// has at least one ResumptionStubEntryPoint fixup. /// - public static void HasResumptionStubFixup(ReadyToRunReader reader, string methodName) - { - HasFixupKindOnMethod(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint, methodName); - } + public static bool HasResumptionStubFixup(ReadyToRunReader reader, string methodName, out string diagnostic) + => HasFixupKindOnMethod(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint, methodName, out diagnostic); /// - /// Asserts the R2R image contains at least one fixup of the given kind. + /// Returns true if the R2R image contains at least one fixup of the given kind. /// - public static void HasFixupKind(ReadyToRunReader reader, ReadyToRunFixupKind kind) + public static bool HasFixupKind(ReadyToRunReader reader, ReadyToRunFixupKind kind, out string diagnostic) { var presentKinds = new HashSet(); foreach (var method in GetAllMethods(reader)) @@ -328,16 +361,19 @@ public static void HasFixupKind(ReadyToRunReader reader, ReadyToRunFixupKind kin } } - Assert.True(presentKinds.Contains(kind), - $"Expected fixup kind '{kind}' not found. " + - $"Present kinds: [{string.Join(", ", presentKinds)}]"); + bool found = presentKinds.Contains(kind); + diagnostic = found + ? $"Found fixup kind '{kind}'." + : $"Expected fixup kind '{kind}' not found. " + + $"Present kinds: [{string.Join(", ", presentKinds)}]"; + return found; } /// - /// Asserts a method whose signature contains + /// Returns true if a method whose signature contains /// has at least one fixup of the given kind. /// - public static void HasFixupKindOnMethod(ReadyToRunReader reader, ReadyToRunFixupKind kind, string methodName) + public static bool HasFixupKindOnMethod(ReadyToRunReader reader, ReadyToRunFixupKind kind, string methodName, out string diagnostic) { var methodsWithFixup = new List(); foreach (var method in GetAllMethods(reader)) @@ -359,13 +395,17 @@ public static void HasFixupKindOnMethod(ReadyToRunReader reader, ReadyToRunFixup { methodsWithFixup.Add(method.SignatureString); if (method.SignatureString.Contains(methodName, StringComparison.OrdinalIgnoreCase)) - return; + { + diagnostic = $"Found '{kind}' fixup on method matching '{methodName}'."; + return true; + } } } - Assert.Fail( + diagnostic = $"Expected fixup kind '{kind}' on method matching '{methodName}', but not found.\n" + - $"Methods with '{kind}' fixups: [{string.Join(", ", methodsWithFixup)}]"); + $"Methods with '{kind}' fixups: [{string.Join(", ", methodsWithFixup)}]"; + return false; } } From 0f5280dda3de0dc7f4c90182136c7618a2301a6a Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:08:32 -0700 Subject: [PATCH 48/74] Add CompositeAsyncGenericTypes regression test Composite-mode regression coverage for async thunk emission of methods on generic types (and generic methods on generic types). The parent PR's description specifically calls these out as the case that originally broke MethodWithToken..ctor() owning-type computation when the async-thunk ILStub forced a strip- instantiation in CorInfoImpl.HandleToModuleToken; the follow-up "Get IL for the (possibly instantiated) method, not the definition" fix in EnsureAsyncThunkTokensAreAvailable is also exercised because it only matters for instantiated methods/types. The test covers: - GenericContainer.GetValueAsync (value-type instantiation) - GenericContainer.GetValueAsync (canonical/__Canon) - GenericContainer.CombineAsync (generic method on generic type, value-type args) - GenericContainer.CombineAsync (generic method on generic type, canonical args) Async-variant assertions include the generic-arg instantiations in the substring match (e.g. GenericContainer`1.CombineAsync ) so the test would fail if only the open generic signature were emitted and not the instantiated forms. Also corrects the XML-doc on CompositeDoesNotProduceCrossModuleInliningInfo: the prior wording implied composite mode itself precludes CrossModuleInlineInfo, which is too strong. Composite output combined with an external --opt-cross-module reference can still produce CrossModuleInlineInfo entries; the test only covers the 'all inlinees are composite inputs' case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 88 +++++++++++++++++-- .../CompositeAsyncGenericTypesMain.cs | 43 +++++++++ .../Dependencies/AsyncGenericTypeLib.cs | 31 +++++++ 3 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 2196d996705e53..1816bdd4511027 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -416,12 +416,15 @@ static void Validate(ReadyToRunReader reader) } /// - /// Negative test: composite mode does NOT produce a CrossModuleInlineInfo section. - /// True cross-module inlining (recorded in CrossModuleInlineInfo) requires the inlinee - /// to live in a module OUTSIDE the version bubble (added via --opt-cross-module on a - /// reference assembly). In composite mode, all input modules are part of the same - /// version bubble, so cross-module inlining info is never produced — inlining between - /// composite inputs is recorded in the per-module InliningInfo2 section instead. + /// Negative test: a composite image whose only inputs are the inlinee and the inliner + /// does NOT produce a CrossModuleInlineInfo section. CrossModuleInlineInfo only records + /// inlining where the inlinee module is OUTSIDE the compiled image's version bubble + /// (typically added via --opt-cross-module on a reference assembly). Here both + /// modules are composite inputs and therefore in the same version bubble, so any + /// inlining between them is recorded in the per-module InliningInfo2 section instead. + /// A different setup — composite output plus an external reference passed via + /// --opt-cross-module — could still produce CrossModuleInlineInfo entries; this + /// test only covers the "all inlinees are composite inputs" case. /// Compare with , which uses the same source modules /// in a non-composite layout and DOES produce CrossModuleInlineInfo entries. /// @@ -641,6 +644,79 @@ static void Validate(ReadyToRunReader reader) } } + /// + /// Composite-mode regression coverage for async thunk emission of methods on + /// generic types (and generic methods on generic types). The parent PR's + /// description specifically calls these out as the case that originally + /// broke MethodWithToken..ctor() owning-type computation when the + /// async-thunk ILStub forced a strip-instantiation in + /// CorInfoImpl.HandleToModuleToken. The follow-up "Get IL for the + /// (possibly instantiated) method, not the definition" fix in + /// ReadyToRunCodegenCompilation.EnsureAsyncThunkTokensAreAvailable + /// is also exercised by this test (it only matters for instantiated + /// methods/types). Both reference-type and value-type instantiations are + /// covered because token resolution differs between the two. + /// + [Fact] + public void CompositeAsyncGenericTypes() + { + var asyncGenericTypeLib = new CompiledAssembly + { + AssemblyName = "AsyncGenericTypeLib", + SourceResourceNames = + [ + "RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + }; + var compositeAsyncGenericTypesMain = new CompiledAssembly + { + AssemblyName = "CompositeAsyncGenericTypesMain", + SourceResourceNames = + [ + "RuntimeAsync/CompositeAsyncGenericTypesMain.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + References = [asyncGenericTypeLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeAsyncGenericTypes), + [ + new(nameof(CompositeAsyncGenericTypes), + [ + new CrossgenAssembly(asyncGenericTypeLib), + new CrossgenAssembly(compositeAsyncGenericTypesMain), + ]) + { + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + string diag; + // Async thunks for the consumer's instantiated callers. + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallGenericContainerInt", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallGenericContainerString", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallGenericMethodOnGenericTypeIntLong", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallGenericMethodOnGenericTypeStringObject", out diag), diag); + + // Async thunks for the library's generic-type methods, asserted with their + // generic-arg instantiations to ensure we aren't matching only the open + // (unspecialized) method signature. Reference-type instantiations are shared + // through the canonical (__Canon) form, so the string consumer's calls also + // produce the __Canon variant rather than a separate entry. + Assert.True(R2RAssert.HasAsyncVariant(reader, "GenericContainer`1.GetValueAsync", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "GenericContainer`1<__Canon>.GetValueAsync", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "GenericContainer`1.CombineAsync", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "GenericContainer`1<__Canon>.CombineAsync<__Canon>", out diag), diag); + } + } + /// /// Non-composite runtime-async + cross-module inlining where the inlinee /// captures GC refs across await points. Validates that ContinuationLayout diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs new file mode 100644 index 00000000000000..8dbd03c5de1fd1 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs @@ -0,0 +1,43 @@ +// Test: Composite-mode async thunk emission for async methods on generic types +// (and generic async methods on generic types). +// +// Regression coverage for the MethodWithToken/OwningType handling described in +// the parent PR ("Enable compilation of async thunks in composite mode") and +// for the follow-up "Get IL for the (possibly instantiated) method, not the +// definition" fix in ReadyToRunCodegenCompilation.cs. Both code paths only run +// for instantiated methods on instantiated generic types compiled inside a +// composite image. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class CompositeAsyncGenericTypesMain +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallGenericContainerInt() + { + var c = new GenericContainer(42); + return await c.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallGenericContainerString() + { + var c = new GenericContainer("hello"); + return await c.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallGenericMethodOnGenericTypeIntLong() + { + var c = new GenericContainer(7); + return await c.CombineAsync(11L); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallGenericMethodOnGenericTypeStringObject() + { + var c = new GenericContainer("k"); + return await c.CombineAsync("v"); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs new file mode 100644 index 00000000000000..cfd058d733c3ea --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs @@ -0,0 +1,31 @@ +// Dependency library for composite-mode generics-on-async-thunks regression tests. +// Provides: +// - GenericContainer: a generic type with both a non-generic and a generic +// async method. The generic-method-on-generic-type combination is the exact +// scenario called out by the parent PR's description as the case that +// originally broke MethodWithToken/OwningType resolution when emitting async +// thunks in composite mode. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public class GenericContainer +{ + private readonly T _value; + + public GenericContainer(T value) => _value = value; + + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task GetValueAsync() + { + await Task.Yield(); + return _value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task CombineAsync(U seed) + { + await Task.Yield(); + return $"{_value}+{seed}"; + } +} From c224b67f80f63121449f38111bf9aa41c293a01b Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:15:07 -0700 Subject: [PATCH 49/74] Dedupe R2R test fixtures: LocalsCapturedAcrossAwait & InlinableAsyncMethods Merge duplicate test-asset files that described the same code in composite vs. non-composite wiring. Rename asset files, types, and methods to describe what the code does (not the test scenario), drop Lib/DepLib/Composite/Cont suffixes, and put consumer-specific deps under Dependencies/
..cs. * AsyncDepLibContinuation.cs + AsyncCompositeContLib.cs + AsyncCrossModule- Continuation.cs + CompositeAsyncContinuationMain.cs collapse into one consumer (AwaitsLocalsCapturedAcrossAwait.cs) over one dep. * AsyncDepLib.cs + AsyncInlineableLib.cs + AsyncCompositeLib.cs collapse into InlinableAsyncMethods, with a single consumer AwaitsInlinableAsync. Tests (AsyncCrossModuleContinuation, CompositeAsyncContinuationAndResume, AsyncCrossModuleInlining, RuntimeAsyncCrossModule, CompositeAsync, and MultiStepCompositeAndNonCompositeAsync) are rewired to the new paths, AssemblyNames, and renamed method identifiers in their validators. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CrossModuleInlining/AsyncMethods.cs | 27 ---- .../CrossModuleInlining/CompositeAsync.cs | 26 ---- .../Dependencies/AsyncInlineableLib.cs | 15 -- .../TestCases/R2RTestSuites.cs | 142 +++++++++--------- .../RuntimeAsync/AsyncCrossModule.cs | 28 ---- .../AsyncCrossModuleContinuation.cs | 21 --- .../RuntimeAsync/AwaitsInlinableAsync.cs | 27 ++++ .../AwaitsLocalsCapturedAcrossAwait.cs | 29 ++++ .../CompositeAsyncContinuationMain.cs | 30 ---- .../Dependencies/AsyncCompositeContLib.cs | 28 ---- .../RuntimeAsync/Dependencies/AsyncDepLib.cs | 28 ---- ...tsInlinableAsync.InlinableAsyncMethods.cs} | 8 +- ...dAcrossAwait.LocalsCapturedAcrossAwait.cs} | 14 +- 13 files changed, 138 insertions(+), 285 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/{CrossModuleInlining/Dependencies/AsyncCompositeLib.cs => RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs} (71%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{AsyncDepLibContinuation.cs => AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs} (54%) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs deleted file mode 100644 index fdf1b159130b95..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Test: Cross-module inlining of async methods -// Validates that async methods from AsyncInlineableLib are cross-module -// inlined into this assembly with CHECK_IL_BODY fixups. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncMethods -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task TestAsyncInline() - { - return await AsyncInlineableLib.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task TestAsyncStringInline() - { - return await AsyncInlineableLib.GetStringAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static int TestSyncFromAsyncLib() - { - return AsyncInlineableLib.GetValueSync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs deleted file mode 100644 index 7e19cc7eea200c..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Test: Composite mode with runtime-async methods across assemblies. -// Validates that async methods produce [ASYNC] variants in composite output. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class CompositeAsyncMain -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCompositeAsync() - { - return await AsyncCompositeLib.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCompositeStringAsync() - { - return await AsyncCompositeLib.GetStringAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static int CallCompositeSync() - { - return AsyncCompositeLib.GetValueSync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs deleted file mode 100644 index 153804e6279fc1..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncInlineableLib -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetValueAsync() => 42; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetStringAsync() => "Hello from async"; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetValueSync() => 42; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 1816bdd4511027..af0d1f95e5b3a9 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -102,16 +102,16 @@ public void TransitiveReferences() [Fact] public void AsyncCrossModuleInlining() { - var asyncInlineableLib = new CompiledAssembly + var inlinableAsyncMethods = new CompiledAssembly { - AssemblyName = "AsyncInlineableLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncInlineableLib.cs"], + AssemblyName = "InlinableAsyncMethods", + SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs"], }; - var asyncCrossModuleInlining = new CompiledAssembly + var awaitsInlinableAsync = new CompiledAssembly { AssemblyName = nameof(AsyncCrossModuleInlining), - SourceResourceNames = ["CrossModuleInlining/AsyncMethods.cs"], - References = [asyncInlineableLib] + SourceResourceNames = ["RuntimeAsync/AwaitsInlinableAsync.cs"], + References = [inlinableAsyncMethods] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -119,8 +119,8 @@ public void AsyncCrossModuleInlining() [ new(nameof(AsyncCrossModuleInlining), [ - new CrossgenAssembly(asyncCrossModuleInlining), - new CrossgenAssembly(asyncInlineableLib) + new CrossgenAssembly(awaitsInlinableAsync), + new CrossgenAssembly(inlinableAsyncMethods) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -134,8 +134,8 @@ public void AsyncCrossModuleInlining() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "AsyncInlineableLib", out diag), diag); - Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestAsyncInline", "GetValueAsync", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlinableAsyncMethods", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "CallGetValueAsync", "GetValueAsync", out diag), diag); } } @@ -322,12 +322,12 @@ static void Validate(ReadyToRunReader reader) [Fact] public void RuntimeAsyncCrossModule() { - var asyncDepLib = new CompiledAssembly + var inlinableAsyncMethods = new CompiledAssembly { - AssemblyName = "AsyncDepLib", + AssemblyName = "InlinableAsyncMethods", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AsyncDepLib.cs", + "RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -337,11 +337,11 @@ public void RuntimeAsyncCrossModule() AssemblyName = nameof(RuntimeAsyncCrossModule), SourceResourceNames = [ - "RuntimeAsync/AsyncCrossModule.cs", + "RuntimeAsync/AwaitsInlinableAsync.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [asyncDepLib] + References = [inlinableAsyncMethods] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -350,7 +350,7 @@ public void RuntimeAsyncCrossModule() new(nameof(RuntimeAsyncCrossModule), [ new CrossgenAssembly(runtimeAsyncCrossModule), - new CrossgenAssembly(asyncDepLib) + new CrossgenAssembly(inlinableAsyncMethods) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -364,8 +364,8 @@ public void RuntimeAsyncCrossModule() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLib", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleAsync", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlinableAsyncMethods", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallGetValueAsync", out diag), diag); } } @@ -476,18 +476,18 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeAsync() { - var asyncCompositeLib = new CompiledAssembly + var inlinableAsyncMethods = new CompiledAssembly { - AssemblyName = "AsyncCompositeLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], + AssemblyName = "InlinableAsyncMethods", + SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs"], Features = { RuntimeAsyncFeature }, }; - var compositeAsyncMain = new CompiledAssembly + var awaitsInlinableAsync = new CompiledAssembly { - AssemblyName = "CompositeAsyncMain", - SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], + AssemblyName = "AwaitsInlinableAsync", + SourceResourceNames = ["RuntimeAsync/AwaitsInlinableAsync.cs"], Features = { RuntimeAsyncFeature }, - References = [asyncCompositeLib] + References = [inlinableAsyncMethods] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -495,8 +495,8 @@ public void CompositeAsync() [ new(nameof(CompositeAsync), [ - new CrossgenAssembly(asyncCompositeLib), - new CrossgenAssembly(compositeAsyncMain), + new CrossgenAssembly(inlinableAsyncMethods), + new CrossgenAssembly(awaitsInlinableAsync), ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], @@ -507,15 +507,15 @@ public void CompositeAsync() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "AsyncCompositeLib", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlinableAsyncMethods", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallGetValueAsync", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "GetValueAsync", out diag), diag); } } /// /// The full intersection: composite + runtime-async + cross-module inlining. - /// Async methods from AsyncCompositeLib are inlined into CompositeAsyncMain + /// Async methods from InlinableAsyncMethods are inlined into AwaitsInlinableAsync /// within a composite image, exercising MutableModule token encoding for /// cross-module async continuation layouts. /// @@ -587,26 +587,26 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeAsyncContinuationAndResume() { - var asyncCompositeContLib = new CompiledAssembly + var locals = new CompiledAssembly { - AssemblyName = "AsyncCompositeContLib", + AssemblyName = "LocalsCapturedAcrossAwait", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AsyncCompositeContLib.cs", + "RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, }; - var compositeAsyncContMain = new CompiledAssembly + var awaitsLocals = new CompiledAssembly { - AssemblyName = "CompositeAsyncContinuationMain", + AssemblyName = "AwaitsLocalsCapturedAcrossAwait", SourceResourceNames = [ - "RuntimeAsync/CompositeAsyncContinuationMain.cs", + "RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [asyncCompositeContLib] + References = [locals] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -614,8 +614,8 @@ public void CompositeAsyncContinuationAndResume() [ new(nameof(CompositeAsyncContinuationAndResume), [ - new CrossgenAssembly(asyncCompositeContLib), - new CrossgenAssembly(compositeAsyncContMain), + new CrossgenAssembly(locals), + new CrossgenAssembly(awaitsLocals), ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], @@ -627,19 +627,19 @@ static void Validate(ReadyToRunReader reader) { string diag; // Library methods produce async variants and resume stubs - Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefComposite", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureArrayComposite", out diag), diag); - Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureRefComposite", out diag), diag); - Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureArrayComposite", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureArrayAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureRefAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureArrayAcrossAwait", out diag), diag); // Main assembly methods produce async variants and resume stubs - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureRefComposite", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureRefAcrossAwait", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "LocalCaptureAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasResumptionStub(reader, "CallCaptureRefComposite", out diag), diag); + Assert.True(R2RAssert.HasResumptionStub(reader, "CallCaptureRefAcrossAwait", out diag), diag); Assert.True(R2RAssert.HasResumptionStub(reader, "LocalCaptureAcrossAwait", out diag), diag); // ContinuationLayout fixups are present for methods with GC refs across awaits - Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureRefComposite", out diag), diag); + Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureRefAcrossAwait", out diag), diag); Assert.True(R2RAssert.HasContinuationLayout(reader, "LocalCaptureAcrossAwait", out diag), diag); } } @@ -725,26 +725,26 @@ static void Validate(ReadyToRunReader reader) [Fact] public void AsyncCrossModuleContinuation() { - var asyncDepLibCont = new CompiledAssembly + var locals = new CompiledAssembly { - AssemblyName = "AsyncDepLibContinuation", + AssemblyName = "LocalsCapturedAcrossAwait", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs", + "RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, }; - var asyncCrossModuleCont = new CompiledAssembly + var awaitsLocals = new CompiledAssembly { AssemblyName = nameof(AsyncCrossModuleContinuation), SourceResourceNames = [ - "RuntimeAsync/AsyncCrossModuleContinuation.cs", + "RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [asyncDepLibCont] + References = [locals] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -752,8 +752,8 @@ public void AsyncCrossModuleContinuation() [ new(nameof(AsyncCrossModuleContinuation), [ - new CrossgenAssembly(asyncCrossModuleCont), - new CrossgenAssembly(asyncDepLibCont) + new CrossgenAssembly(awaitsLocals), + new CrossgenAssembly(locals) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -767,9 +767,9 @@ public void AsyncCrossModuleContinuation() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureArray", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "LocalsCapturedAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureRefAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureArrayAcrossAwait", out diag), diag); } } @@ -1060,16 +1060,16 @@ static void Validate(ReadyToRunReader reader) [Fact] public void MultiStepCompositeAndNonCompositeAsync() { - var asyncDepLib = new CompiledAssembly + var locals = new CompiledAssembly { - AssemblyName = "AsyncDepLibContinuation", - SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs"], + AssemblyName = "LocalsCapturedAcrossAwait", + SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs"], Features = { RuntimeAsyncFeature }, }; - var asyncCompositeLib = new CompiledAssembly + var inlinableAsyncMethods = new CompiledAssembly { - AssemblyName = "AsyncCompositeLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], + AssemblyName = "InlinableAsyncMethods", + SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs"], Features = { RuntimeAsyncFeature }, }; var asyncConsumer = new CompiledAssembly @@ -1077,11 +1077,11 @@ public void MultiStepCompositeAndNonCompositeAsync() AssemblyName = "MultiStepAsyncConsumer", SourceResourceNames = [ - "RuntimeAsync/AsyncCrossModuleContinuation.cs", + "RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [asyncDepLib] + References = [locals] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -1089,22 +1089,22 @@ public void MultiStepCompositeAndNonCompositeAsync() [ new("CompositeAsyncStep", [ - new CrossgenAssembly(asyncDepLib), - new CrossgenAssembly(asyncCompositeLib), + new CrossgenAssembly(locals), + new CrossgenAssembly(inlinableAsyncMethods), ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = reader => { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "LocalsCapturedAcrossAwait", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefAcrossAwait", out diag), diag); }, }, new("NonCompositeAsyncStep", [ new CrossgenAssembly(asyncConsumer), - new CrossgenAssembly(asyncDepLib) + new CrossgenAssembly(locals) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -1114,8 +1114,8 @@ public void MultiStepCompositeAndNonCompositeAsync() Validate = reader => { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "LocalsCapturedAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureRefAcrossAwait", out diag), diag); }, }, ])); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs deleted file mode 100644 index cc1853b48a5bb7..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Test: Cross-module async method inlining -// Validates that cross-module async compilation produces manifest refs -// and [ASYNC] variants for methods calling into a dependency library. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncCrossModule -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCrossModuleAsync() - { - return await AsyncDepLib.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCrossModuleStringAsync() - { - return await AsyncDepLib.GetStringAsync(); - } - - // Call a non-async sync method from async lib - [MethodImpl(MethodImplOptions.NoInlining)] - public static int CallCrossModuleSync() - { - return AsyncDepLib.GetValueSync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs deleted file mode 100644 index b4e2f87ecda45d..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Test: Non-composite runtime-async cross-module inlining with continuation layouts. -// The dependency methods capture GC refs across await points. -// Validates manifest refs and [ASYNC] variants for cross-module async calls. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncCrossModuleContinuation -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCrossModuleCaptureRef() - { - return await AsyncDepLibContinuation.CaptureRefAcrossAwait(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCrossModuleCaptureArray() - { - return await AsyncDepLibContinuation.CaptureArrayAcrossAwait(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs new file mode 100644 index 00000000000000..ca87d90cb855f1 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs @@ -0,0 +1,27 @@ +// Awaits each method in InlinableAsyncMethods so that callers see both +// cross-module inlining opportunities and composite-mode async variant +// emission, depending on how the tests wire the compilation. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AwaitsInlinableAsync +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallGetValueAsync() + { + return await InlinableAsyncMethods.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallGetStringAsync() + { + return await InlinableAsyncMethods.GetStringAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CallGetValueSync() + { + return InlinableAsyncMethods.GetValueSync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs new file mode 100644 index 00000000000000..53e5de86c7c8e3 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs @@ -0,0 +1,29 @@ +// Awaits the methods in LocalsCapturedAcrossAwait, plus a self-contained +// async method that captures locals across an await in this assembly itself. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AwaitsLocalsCapturedAcrossAwait +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCaptureRefAcrossAwait() + { + return await LocalsCapturedAcrossAwait.CaptureRefAcrossAwait(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCaptureArrayAcrossAwait() + { + return await LocalsCapturedAcrossAwait.CaptureArrayAcrossAwait(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task LocalCaptureAcrossAwait() + { + object o = new object(); + string s = "local"; + await Task.Yield(); + return s.Length + o.GetHashCode(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs deleted file mode 100644 index 2419d72ac82af4..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Test: Composite mode async with continuation layouts and resumption stubs. -// Calls async methods from AsyncCompositeContLib that capture GC refs across -// await points, exercising composite-mode ContinuationLayout and RESUME stub emission. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class CompositeAsyncContinuationMain -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCaptureRefComposite() - { - return await AsyncCompositeContLib.CaptureRefComposite(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCaptureArrayComposite() - { - return await AsyncCompositeContLib.CaptureArrayComposite(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task LocalCaptureAcrossAwait() - { - object o = new object(); - string s = "local"; - await Task.Yield(); - return s.Length + o.GetHashCode(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs deleted file mode 100644 index 85ff77178e7502..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Dependency library for composite async continuation tests. -// Contains runtime-async methods that capture GC refs across await points. -// Used in composite mode to exercise MutableModule token encoding for -// cross-module async continuation layouts and resumption stubs. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncCompositeContLib -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CaptureRefComposite() - { - object o = new object(); - string s = "composite_ref"; - await Task.Yield(); - return s + o.GetHashCode(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CaptureArrayComposite() - { - int[] arr = new int[] { 5, 10, 15 }; - string label = "total"; - await Task.Yield(); - return arr[0] + arr[1] + label.Length; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs deleted file mode 100644 index bb10453f70b05e..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Dependency library for async cross-module tests. -// Contains runtime-async methods that should be inlineable. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncDepLib -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetValueAsync() - { - await Task.Yield(); - return 42; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetStringAsync() - { - await Task.Yield(); - return "async_hello"; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetValueSync() - { - return 99; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs similarity index 71% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs index bece15b74b11b6..ca4e53889e480d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs @@ -1,10 +1,10 @@ -// Dependency library for composite async tests. -// Contains runtime-async methods called from another assembly in composite mode. +// Inlinable runtime-async methods used to exercise cross-module inlining +// and composite-mode emission of async variants. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class AsyncCompositeLib +public static class InlinableAsyncMethods { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static async Task GetValueAsync() @@ -17,7 +17,7 @@ public static async Task GetValueAsync() public static async Task GetStringAsync() { await Task.Yield(); - return "composite_async"; + return "hello_from_async"; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs similarity index 54% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs index b4124b80008fe1..a7c3e0cbac13e0 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs @@ -1,22 +1,22 @@ -// Dependency library for async cross-module continuation tests. -// Contains runtime-async methods that capture GC refs across await points, -// forcing ContinuationLayout fixup emission when cross-module inlined. +// Runtime-async methods that capture GC refs across an await point. +// These force ContinuationLayout fixup emission when they are called +// (and optionally cross-module inlined) by another assembly. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class AsyncDepLibContinuation +public static class LocalsCapturedAcrossAwait { - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CaptureRefAcrossAwait() { object o = new object(); - string s = "cross_module"; + string s = "captured"; await Task.Yield(); return s + o.GetHashCode(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CaptureArrayAcrossAwait() { int[] arr = new int[] { 10, 20, 30 }; From ef7078088e22548666afb2f05e8a5722af649888 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:16:19 -0700 Subject: [PATCH 50/74] Dedupe R2R async devirt test fixtures: AwaitsThroughInterface Collapse AsyncDevirtualize.cs + AsyncInterfaceLib.cs + CompositeAsyncDevirtMain.cs into one consumer/dep pair (AwaitsThroughInterface.cs + AwaitsThroughInterface.InterfaceAndImpls.cs). Drop 'Composite'/'Service' from type names (IAsyncService, SealedImpl, OpenImpl). Rewire RuntimeAsyncDevirtualize (single-assembly, pulls both files in) and CompositeAsyncDevirtualize (two-assembly split) to the new paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 23 ++++---- .../RuntimeAsync/AsyncDevirtualize.cs | 56 ------------------- .../RuntimeAsync/AwaitsThroughInterface.cs | 28 ++++++++++ .../RuntimeAsync/CompositeAsyncDevirtMain.cs | 22 -------- ...aitsThroughInterface.InterfaceAndImpls.cs} | 13 +++-- 5 files changed, 47 insertions(+), 95 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsThroughInterface.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{AsyncInterfaceLib.cs => AwaitsThroughInterface.InterfaceAndImpls.cs} (56%) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index af0d1f95e5b3a9..51241a94d42050 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -258,7 +258,8 @@ public void RuntimeAsyncDevirtualize() AssemblyName = nameof(RuntimeAsyncDevirtualize), SourceResourceNames = [ - "RuntimeAsync/AsyncDevirtualize.cs", + "RuntimeAsync/AwaitsThroughInterface.cs", + "RuntimeAsync/Dependencies/AwaitsThroughInterface.InterfaceAndImpls.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -845,22 +846,22 @@ public void MultiStepCompositeAndNonComposite() [Fact] public void CompositeAsyncDevirtualize() { - var asyncInterfaceLib = new CompiledAssembly + var interfaceAndImpls = new CompiledAssembly { - AssemblyName = "AsyncInterfaceLib", - SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncInterfaceLib.cs"], + AssemblyName = "InterfaceAndImpls", + SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsThroughInterface.InterfaceAndImpls.cs"], Features = { RuntimeAsyncFeature }, }; - var compositeDevirtMain = new CompiledAssembly + var awaitsThroughInterface = new CompiledAssembly { - AssemblyName = "CompositeAsyncDevirtMain", + AssemblyName = "AwaitsThroughInterface", SourceResourceNames = [ - "RuntimeAsync/CompositeAsyncDevirtMain.cs", + "RuntimeAsync/AwaitsThroughInterface.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [asyncInterfaceLib] + References = [interfaceAndImpls] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -868,8 +869,8 @@ public void CompositeAsyncDevirtualize() [ new(nameof(CompositeAsyncDevirtualize), [ - new CrossgenAssembly(asyncInterfaceLib), - new CrossgenAssembly(compositeDevirtMain), + new CrossgenAssembly(interfaceAndImpls), + new CrossgenAssembly(awaitsThroughInterface), ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], @@ -880,7 +881,7 @@ public void CompositeAsyncDevirtualize() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "AsyncInterfaceLib", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InterfaceAndImpls", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "CallOnSealed", out diag), diag); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs deleted file mode 100644 index 6170e4ac67360a..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Test: Async virtual method devirtualization in R2R -// Validates that async methods on sealed/interface types -// produce [ASYNC] variant entries in the R2R image. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public interface IAsyncService -{ - Task GetValueAsync(); -} - -public class OpenImpl : IAsyncService -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public virtual async Task GetValueAsync() - { - await Task.Yield(); - return 10; - } -} - -public sealed class SealedImpl : IAsyncService -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public async Task GetValueAsync() - { - await Task.Yield(); - return 20; - } -} - -public static class AsyncDevirtualize -{ - // Sealed type known at compile time — should devirtualize - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallOnSealed(SealedImpl obj) - { - return await obj.GetValueAsync(); - } - - // newobj gives exact type info — should devirtualize through resolveVirtualMethod - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallOnNewOpen() - { - IAsyncService svc = new OpenImpl(); - return await svc.GetValueAsync(); - } - - // Generic constrained dispatch - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallGenericConstrained(T obj) where T : IAsyncService - { - return await obj.GetValueAsync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsThroughInterface.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsThroughInterface.cs new file mode 100644 index 00000000000000..aacb26c429503d --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsThroughInterface.cs @@ -0,0 +1,28 @@ +// Calls an async interface method via three dispatch shapes: sealed type, +// newobj-then-interface, and generic-constrained. Consumers of +// InterfaceAndImpls to exercise devirtualization of async dispatch. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AwaitsThroughInterface +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallOnSealed(SealedImpl obj) + { + return await obj.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallOnNewOpen() + { + IAsyncService svc = new OpenImpl(); + return await svc.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallGenericConstrained(T obj) where T : IAsyncService + { + return await obj.GetValueAsync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs deleted file mode 100644 index 62a06bc816f0c2..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Test: Composite mode async devirtualization across module boundaries. -// Interface defined in AsyncInterfaceLib, call sites here. -// In composite mode, crossgen2 should devirtualize sealed type dispatch. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class CompositeAsyncDevirtMain -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallOnSealed(SealedAsyncService svc) - { - return await svc.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallOnNewOpen() - { - IAsyncCompositeService svc = new OpenAsyncService(); - return await svc.GetValueAsync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsThroughInterface.InterfaceAndImpls.cs similarity index 56% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsThroughInterface.InterfaceAndImpls.cs index 528162b5d10c10..327130ca8d1605 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsThroughInterface.InterfaceAndImpls.cs @@ -1,25 +1,26 @@ -// Dependency library: defines an async interface and sealed implementation -// for cross-module async devirtualization tests in composite mode. +// Interface + two implementations (sealed and non-sealed/open) of an +// async-returning method. Used by consumers to exercise devirtualization +// of async interface dispatch. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -public interface IAsyncCompositeService +public interface IAsyncService { Task GetValueAsync(); } -public sealed class SealedAsyncService : IAsyncCompositeService +public sealed class SealedImpl : IAsyncService { [MethodImpl(MethodImplOptions.NoInlining)] public async Task GetValueAsync() { await Task.Yield(); - return 42; + return 20; } } -public class OpenAsyncService : IAsyncCompositeService +public class OpenImpl : IAsyncService { [MethodImpl(MethodImplOptions.NoInlining)] public virtual async Task GetValueAsync() From 49bb7744554b248a01f62f32fd688a82532c41a1 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:19:10 -0700 Subject: [PATCH 51/74] Dedupe R2R transitive test fixtures: share leaf between sync + async chains Unify the sync and async transitive reference fixtures so the leaf module (SyncLeafMethods) is shared by both chains: * ExternalLib + AsyncExternalLib -> SyncLeafMethods (leaf with int values and an ExternalType carrying both Value and Label). * InlineableLibTransitive -> InlinableLeafCallers (mid-layer, sync). * AsyncTransitiveLib -> InlinableAsyncLeafCallers (mid-layer, async). * AsyncTransitiveMain -> AwaitsTransitiveAsync (consumer, async). * TransitiveReferences.cs (sync consumer) stays; now references InlinableLeafCallers. Rewire TransitiveReferences, CompositeTransitive, AsyncCrossModuleTransitive, and CompositeAsyncTransitive tests to the new paths, assembly names, and manifest-ref expectations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Dependencies/InlineableLibTransitive.cs | 14 --- .../{ExternalLib.cs => SyncLeafMethods.cs} | 5 +- ...ansitiveReferences.InlinableLeafCallers.cs | 17 +++ .../TransitiveReferences.cs | 12 +-- .../TestCases/R2RTestSuites.cs | 100 +++++++++--------- .../RuntimeAsync/AsyncTransitiveMain.cs | 21 ---- .../RuntimeAsync/AwaitsTransitiveAsync.cs | 20 ++++ .../Dependencies/AsyncExternalLib.cs | 13 --- ...nsitiveAsync.InlinableAsyncLeafCallers.cs} | 10 +- 9 files changed, 102 insertions(+), 110 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{ExternalLib.cs => SyncLeafMethods.cs} (58%) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/TransitiveReferences.InlinableLeafCallers.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsTransitiveAsync.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{AsyncTransitiveLib.cs => AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs} (55%) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs deleted file mode 100644 index 15fd29dda19d4b..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -public static class InlineableLibTransitive -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetExternalValue() => ExternalLib.ExternalValue; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetNestedValue() => ExternalLib.Outer.Inner.NestedValue; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExternalLib.ExternalType CreateExternal() => new ExternalLib.ExternalType { Value = 42 }; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncLeafMethods.cs similarity index 58% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncLeafMethods.cs index d56f2880564a13..dffb22616202f8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncLeafMethods.cs @@ -1,12 +1,15 @@ +// Leaf data types and values used by other cross-module fixtures to +// exercise transitive references. using System; -public static class ExternalLib +public static class SyncLeafMethods { public static int ExternalValue => 99; public class ExternalType { public int Value { get; set; } + public string Label { get; set; } = "external"; } public class Outer diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/TransitiveReferences.InlinableLeafCallers.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/TransitiveReferences.InlinableLeafCallers.cs new file mode 100644 index 00000000000000..79e33ab247bb9c --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/TransitiveReferences.InlinableLeafCallers.cs @@ -0,0 +1,17 @@ +// Inlinable helpers that forward sync calls through to SyncLeafMethods. +// Used to exercise transitive cross-module inlining. +using System; +using System.Runtime.CompilerServices; + +public static class InlinableLeafCallers +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetExternalValue() => SyncLeafMethods.ExternalValue; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetNestedValue() => SyncLeafMethods.Outer.Inner.NestedValue; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SyncLeafMethods.ExternalType CreateExternal() => + new SyncLeafMethods.ExternalType { Value = 42 }; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs index 25bac820fc3002..2d9de073226274 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs @@ -1,6 +1,6 @@ -// Test: Transitive cross-module references -// Validates that when InlineableLibTransitive is inlined, its references to ExternalLib -// are properly encoded in the R2R image (requiring tokens for both libraries). +// Calls inlinable helpers in another module that transitively touch types +// and members from a further module, so the consumer's R2R image must +// carry tokens for both the mid-layer and leaf modules. using System; using System.Runtime.CompilerServices; @@ -9,18 +9,18 @@ public static class TransitiveReferences [MethodImpl(MethodImplOptions.NoInlining)] public static int TestTransitiveValue() { - return InlineableLibTransitive.GetExternalValue(); + return InlinableLeafCallers.GetExternalValue(); } [MethodImpl(MethodImplOptions.NoInlining)] public static int TestNestedTypeAccess() { - return InlineableLibTransitive.GetNestedValue(); + return InlinableLeafCallers.GetNestedValue(); } [MethodImpl(MethodImplOptions.NoInlining)] public static object TestTransitiveTypeCreation() { - return InlineableLibTransitive.CreateExternal(); + return InlinableLeafCallers.CreateExternal(); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 51241a94d42050..20caccf67bd781 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -59,29 +59,29 @@ static void Validate(ReadyToRunReader reader) [Fact] public void TransitiveReferences() { - var externalLib = new CompiledAssembly() + var syncLeafMethods = new CompiledAssembly() { - AssemblyName = "ExternalLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/ExternalLib.cs"], + AssemblyName = "SyncLeafMethods", + SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncLeafMethods.cs"], }; - var inlineableLibTransitive = new CompiledAssembly() + var inlinableLeafCallers = new CompiledAssembly() { - AssemblyName = "InlineableLibTransitive", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLibTransitive.cs"], - References = [externalLib] + AssemblyName = "InlinableLeafCallers", + SourceResourceNames = ["CrossModuleInlining/Dependencies/TransitiveReferences.InlinableLeafCallers.cs"], + References = [syncLeafMethods] }; var transitiveReferences = new CompiledAssembly() { AssemblyName = "TransitiveReferences", SourceResourceNames = ["CrossModuleInlining/TransitiveReferences.cs"], - References = [inlineableLibTransitive, externalLib] + References = [inlinableLeafCallers, syncLeafMethods] }; new R2RTestRunner(_output).Run(new R2RTestCase(nameof(TransitiveReferences), [ new("TransitiveReferences", [ new CrossgenAssembly(transitiveReferences), - new CrossgenAssembly(externalLib) { Kind = Crossgen2InputKind.Reference }, - new CrossgenAssembly(inlineableLibTransitive) + new CrossgenAssembly(syncLeafMethods) { Kind = Crossgen2InputKind.Reference }, + new CrossgenAssembly(inlinableLeafCallers) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -91,8 +91,8 @@ public void TransitiveReferences() Validate = reader => { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLibTransitive", out diag), diag); - Assert.True(R2RAssert.HasManifestRef(reader, "ExternalLib", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlinableLeafCallers", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "SyncLeafMethods", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestTransitiveValue", "GetExternalValue", out diag), diag); }, }, @@ -893,22 +893,22 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeTransitive() { - var externalLib = new CompiledAssembly + var syncLeafMethods = new CompiledAssembly { - AssemblyName = "ExternalLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/ExternalLib.cs"], + AssemblyName = "SyncLeafMethods", + SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncLeafMethods.cs"], }; - var inlineableLibTransitive = new CompiledAssembly + var inlinableLeafCallers = new CompiledAssembly { - AssemblyName = "InlineableLibTransitive", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLibTransitive.cs"], - References = [externalLib] + AssemblyName = "InlinableLeafCallers", + SourceResourceNames = ["CrossModuleInlining/Dependencies/TransitiveReferences.InlinableLeafCallers.cs"], + References = [syncLeafMethods] }; var compositeTransitiveMain = new CompiledAssembly { AssemblyName = "CompositeTransitive", SourceResourceNames = ["CrossModuleInlining/TransitiveReferences.cs"], - References = [inlineableLibTransitive, externalLib] + References = [inlinableLeafCallers, syncLeafMethods] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -916,8 +916,8 @@ public void CompositeTransitive() [ new(nameof(CompositeTransitive), [ - new CrossgenAssembly(externalLib), - new CrossgenAssembly(inlineableLibTransitive), + new CrossgenAssembly(syncLeafMethods), + new CrossgenAssembly(inlinableLeafCallers), new CrossgenAssembly(compositeTransitiveMain), ]) { @@ -929,8 +929,8 @@ public void CompositeTransitive() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLibTransitive", out diag), diag); - Assert.True(R2RAssert.HasManifestRef(reader, "ExternalLib", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlinableLeafCallers", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "SyncLeafMethods", out diag), diag); } } @@ -941,32 +941,32 @@ static void Validate(ReadyToRunReader reader) [Fact] public void AsyncCrossModuleTransitive() { - var asyncExternalLib = new CompiledAssembly + var syncLeafMethods = new CompiledAssembly { - AssemblyName = "AsyncExternalLib", - SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncExternalLib.cs"], + AssemblyName = "SyncLeafMethods", + SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncLeafMethods.cs"], }; - var asyncTransitiveLib = new CompiledAssembly + var inlinableAsyncLeafCallers = new CompiledAssembly { - AssemblyName = "AsyncTransitiveLib", + AssemblyName = "InlinableAsyncLeafCallers", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AsyncTransitiveLib.cs", + "RuntimeAsync/Dependencies/AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [asyncExternalLib] + References = [syncLeafMethods] }; - var asyncTransitiveMain = new CompiledAssembly + var awaitsTransitiveAsync = new CompiledAssembly { AssemblyName = nameof(AsyncCrossModuleTransitive), SourceResourceNames = [ - "RuntimeAsync/AsyncTransitiveMain.cs", + "RuntimeAsync/AwaitsTransitiveAsync.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [asyncTransitiveLib, asyncExternalLib] + References = [inlinableAsyncLeafCallers, syncLeafMethods] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -974,9 +974,9 @@ public void AsyncCrossModuleTransitive() [ new(nameof(AsyncCrossModuleTransitive), [ - new CrossgenAssembly(asyncTransitiveMain), - new CrossgenAssembly(asyncExternalLib) { Kind = Crossgen2InputKind.Reference }, - new CrossgenAssembly(asyncTransitiveLib) + new CrossgenAssembly(awaitsTransitiveAsync), + new CrossgenAssembly(syncLeafMethods) { Kind = Crossgen2InputKind.Reference }, + new CrossgenAssembly(inlinableAsyncLeafCallers) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -990,7 +990,7 @@ public void AsyncCrossModuleTransitive() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlinableAsyncLeafCallers", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync", out diag), diag); } } @@ -1002,32 +1002,32 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeAsyncTransitive() { - var asyncExternalLib = new CompiledAssembly + var syncLeafMethods = new CompiledAssembly { - AssemblyName = "AsyncExternalLib", - SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncExternalLib.cs"], + AssemblyName = "SyncLeafMethods", + SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncLeafMethods.cs"], }; - var asyncTransitiveLib = new CompiledAssembly + var inlinableAsyncLeafCallers = new CompiledAssembly { - AssemblyName = "AsyncTransitiveLib", + AssemblyName = "InlinableAsyncLeafCallers", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AsyncTransitiveLib.cs", + "RuntimeAsync/Dependencies/AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [asyncExternalLib] + References = [syncLeafMethods] }; var compositeAsyncTransitiveMain = new CompiledAssembly { AssemblyName = "CompositeAsyncTransitive", SourceResourceNames = [ - "RuntimeAsync/AsyncTransitiveMain.cs", + "RuntimeAsync/AwaitsTransitiveAsync.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [asyncTransitiveLib, asyncExternalLib] + References = [inlinableAsyncLeafCallers, syncLeafMethods] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -1035,8 +1035,8 @@ public void CompositeAsyncTransitive() [ new(nameof(CompositeAsyncTransitive), [ - new CrossgenAssembly(asyncExternalLib), - new CrossgenAssembly(asyncTransitiveLib), + new CrossgenAssembly(syncLeafMethods), + new CrossgenAssembly(inlinableAsyncLeafCallers), new CrossgenAssembly(compositeAsyncTransitiveMain), ]) { @@ -1048,7 +1048,7 @@ public void CompositeAsyncTransitive() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlinableAsyncLeafCallers", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync", out diag), diag); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs deleted file mode 100644 index e92cd6f1ae2de4..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Test: Non-composite runtime-async transitive cross-module inlining. -// Chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. -// Validates transitive manifest refs and async cross-module inlining. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncTransitiveMain -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallTransitiveValueAsync() - { - return await AsyncTransitiveLib.GetExternalValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallTransitiveLabelAsync() - { - return await AsyncTransitiveLib.GetExternalLabelAsync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsTransitiveAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsTransitiveAsync.cs new file mode 100644 index 00000000000000..2769922e95a273 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsTransitiveAsync.cs @@ -0,0 +1,20 @@ +// Awaits through InlinableAsyncLeafCallers so the transitive reference to +// SyncLeafMethods is exercised under runtime-async. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AwaitsTransitiveAsync +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallTransitiveValueAsync() + { + return await InlinableAsyncLeafCallers.GetExternalValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallTransitiveLabelAsync() + { + return await InlinableAsyncLeafCallers.GetExternalLabelAsync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs deleted file mode 100644 index f45db23f004570..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs +++ /dev/null @@ -1,13 +0,0 @@ -// External types for async transitive cross-module tests. -// Similar to ExternalLib but with async-friendly types. -using System; - -public static class AsyncExternalLib -{ - public static int ExternalValue => 77; - - public class AsyncExternalType - { - public string Label { get; set; } = "external"; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs similarity index 55% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs index 717040a417c362..04ac8012b0abbd 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs @@ -1,22 +1,22 @@ -// Middle library in async transitive chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. -// Contains runtime-async methods that reference types from AsyncExternalLib. +// Inlinable async helpers that forward through to SyncLeafMethods. +// Used to exercise transitive cross-module inlining for runtime-async. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class AsyncTransitiveLib +public static class InlinableAsyncLeafCallers { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static async Task GetExternalValueAsync() { await Task.Yield(); - return AsyncExternalLib.ExternalValue; + return SyncLeafMethods.ExternalValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static async Task GetExternalLabelAsync() { - var ext = new AsyncExternalLib.AsyncExternalType(); + var ext = new SyncLeafMethods.ExternalType(); await Task.Yield(); return ext.Label; } From d3fe5b8371e71ed70217cebba8c61b9942ce838b Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:20:40 -0700 Subject: [PATCH 52/74] Rename R2R basic-inlining test fixtures to describe code, not scenario * InlineableLib.cs -> BasicInlining.SyncInlinableMethods.cs (class InlineableLib -> SyncInlinableMethods). * CompositeLib.cs -> CompositeBasic.SyncTypeAndMethod.cs (class CompositeLib -> SyncTypeAndMethod). * Update consumers BasicInlining.cs and CompositeBasic.cs and R2RTestSuites.cs wirings / validators accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CrossModuleInlining/BasicInlining.cs | 6 +-- .../CrossModuleInlining/CompositeBasic.cs | 4 +- ... => BasicInlining.SyncInlinableMethods.cs} | 4 +- ...cs => CompositeBasic.SyncTypeAndMethod.cs} | 2 +- .../TestCases/R2RTestSuites.cs | 42 +++++++++---------- 5 files changed, 29 insertions(+), 29 deletions(-) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{InlineableLib.cs => BasicInlining.SyncInlinableMethods.cs} (73%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{CompositeLib.cs => CompositeBasic.SyncTypeAndMethod.cs} (81%) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs index ba301272868c25..0a76a9780b6d05 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs @@ -9,18 +9,18 @@ public static class BasicInlining [MethodImpl(MethodImplOptions.NoInlining)] public static int TestGetValue() { - return InlineableLib.GetValue(); + return SyncInlinableMethods.GetValue(); } [MethodImpl(MethodImplOptions.NoInlining)] public static string TestGetString() { - return InlineableLib.GetString(); + return SyncInlinableMethods.GetString(); } [MethodImpl(MethodImplOptions.NoInlining)] public static int TestAdd() { - return InlineableLib.Add(10, 32); + return SyncInlinableMethods.Add(10, 32); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs index 0ba24d5563555c..abbd5b2131bde2 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs @@ -9,12 +9,12 @@ public static class CompositeBasic [MethodImpl(MethodImplOptions.NoInlining)] public static int TestCompositeCall() { - return CompositeLib.GetCompositeValue(); + return SyncTypeAndMethod.GetCompositeValue(); } [MethodImpl(MethodImplOptions.NoInlining)] public static object TestCompositeTypeCreation() { - return new CompositeLib.CompositeType(); + return new SyncTypeAndMethod.CompositeType(); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs similarity index 73% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs index a799cfed7282e8..51944a1ce2fde4 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs @@ -1,13 +1,13 @@ using System; using System.Runtime.CompilerServices; -public static class InlineableLib +public static class SyncInlinableMethods { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetValue() => 42; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetString() => "Hello from InlineableLib"; + public static string GetString() => "Hello from SyncInlinableMethods"; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Add(int a, int b) => a + b; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeBasic.SyncTypeAndMethod.cs similarity index 81% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeBasic.SyncTypeAndMethod.cs index 2dc5db2de38bcc..2ed46a18a34986 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeBasic.SyncTypeAndMethod.cs @@ -1,6 +1,6 @@ using System; -public static class CompositeLib +public static class SyncTypeAndMethod { public static int GetCompositeValue() => 100; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 20caccf67bd781..a90b8778e713e5 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -26,30 +26,30 @@ public R2RTestSuites(ITestOutputHelper output) [Fact] public void BasicCrossModuleInlining() { - var inlineableLib = new CompiledAssembly + var syncInlinableMethods = new CompiledAssembly { - AssemblyName = "InlineableLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], + AssemblyName = "SyncInlinableMethods", + SourceResourceNames = ["CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs"], }; var basicCrossModuleInlining = new CompiledAssembly { AssemblyName = "BasicCrossModuleInlining", SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], - References = [inlineableLib] + References = [syncInlinableMethods] }; - var cgInlineableLib = new CrossgenAssembly(inlineableLib){ Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization] }; + var cgSyncInlinableMethods = new CrossgenAssembly(syncInlinableMethods){ Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization] }; var cgBasicCrossModuleInlining = new CrossgenAssembly(basicCrossModuleInlining); new R2RTestRunner(_output).Run(new R2RTestCase( nameof(BasicCrossModuleInlining), - [new CrossgenCompilation(basicCrossModuleInlining.AssemblyName, [cgInlineableLib, cgBasicCrossModuleInlining]) { Validate = Validate }]) + [new CrossgenCompilation(basicCrossModuleInlining.AssemblyName, [cgSyncInlinableMethods, cgBasicCrossModuleInlining]) { Validate = Validate }]) ); static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLib", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "SyncInlinableMethods", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetValue", "GetValue", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetString", "GetString", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInliningInfo(reader, out diag), diag); @@ -144,8 +144,8 @@ public void CompositeBasic() { var compositeLib = new CompiledAssembly { - AssemblyName = "CompositeLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/CompositeLib.cs"], + AssemblyName = "SyncTypeAndMethod", + SourceResourceNames = ["CrossModuleInlining/Dependencies/CompositeBasic.SyncTypeAndMethod.cs"], }; var compositeBasic = new CompiledAssembly { @@ -171,7 +171,7 @@ public void CompositeBasic() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "CompositeLib", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "SyncTypeAndMethod", out diag), diag); } } @@ -382,16 +382,16 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeCrossModuleInlining() { - var inlineableLib = new CompiledAssembly + var syncInlinableMethods = new CompiledAssembly { - AssemblyName = "InlineableLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], + AssemblyName = "SyncInlinableMethods", + SourceResourceNames = ["CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs"], }; var compositeMain = new CompiledAssembly { AssemblyName = "CompositeCrossModuleInlining", SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], - References = [inlineableLib] + References = [syncInlinableMethods] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -399,7 +399,7 @@ public void CompositeCrossModuleInlining() [ new(nameof(CompositeCrossModuleInlining), [ - new CrossgenAssembly(inlineableLib), + new CrossgenAssembly(syncInlinableMethods), new CrossgenAssembly(compositeMain), ]) { @@ -411,7 +411,7 @@ public void CompositeCrossModuleInlining() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLib", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "SyncInlinableMethods", out diag), diag); Assert.True(R2RAssert.HasInlinedMethod(reader, "TestGetValue", "GetValue", out diag), diag); } } @@ -432,16 +432,16 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeDoesNotProduceCrossModuleInliningInfo() { - var inlineableLib = new CompiledAssembly + var syncInlinableMethods = new CompiledAssembly { - AssemblyName = "InlineableLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], + AssemblyName = "SyncInlinableMethods", + SourceResourceNames = ["CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs"], }; var compositeMain = new CompiledAssembly { AssemblyName = nameof(CompositeDoesNotProduceCrossModuleInliningInfo), SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], - References = [inlineableLib] + References = [syncInlinableMethods] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -449,7 +449,7 @@ public void CompositeDoesNotProduceCrossModuleInliningInfo() [ new(nameof(CompositeDoesNotProduceCrossModuleInliningInfo), [ - new CrossgenAssembly(inlineableLib), + new CrossgenAssembly(syncInlinableMethods), new CrossgenAssembly(compositeMain), ]) { From 4998999028596809c6bf67c2f89e0f64de4a96a4 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:24:07 -0700 Subject: [PATCH 53/74] Rename remaining R2R test fixtures to describe code, not scenario Filename/class renames (no behavior change): * AsyncNoYield.cs -> AsyncWithoutYield.cs * AsyncInlineCallers.cs (consumer) -> AwaitsInlineCandidateMatrix.cs; AsyncInlineCandidatesLib.cs -> Dependencies/AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs * CompositeAsyncGenericTypesMain.cs -> AwaitsAsyncMethodsOnGenericType.cs; AsyncGenericTypeLib.cs -> Dependencies/AwaitsAsyncMethodsOnGenericType.GenericContainer.cs * MultiStepLibA.cs -> Dependencies/MultiStepConsumer.Leaf.cs (class MultiStepLibA -> MultiStepLeaf) * MultiStepLibB.cs -> Dependencies/MultiStepConsumer.Mid.cs (class MultiStepLibB -> MultiStepMid) * CrossModuleGenericLib.cs -> Dependencies/MultiInlinerConsumer.GenericWrappers.cs AsyncWithContinuation.cs is removed; RuntimeAsyncContinuationLayout now reuses the already-shared AwaitsLocalsCapturedAcrossAwait dep (same behavior, different method names: CaptureObjectAcrossAwait -> CaptureRefAcrossAwait, CaptureMultipleRefsAcrossAwait -> CaptureArrayAcrossAwait). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...> MultiInlinerConsumer.GenericWrappers.cs} | 0 ...iStepLibA.cs => MultiStepConsumer.Leaf.cs} | 2 +- ...tiStepLibB.cs => MultiStepConsumer.Mid.cs} | 8 +-- .../CrossModuleInlining/MultiStepConsumer.cs | 6 +-- .../TestCases/R2RTestSuites.cs | 50 +++++++++---------- .../RuntimeAsync/AsyncWithContinuation.cs | 26 ---------- .../{AsyncNoYield.cs => AsyncWithoutYield.cs} | 2 +- ....cs => AwaitsAsyncMethodsOnGenericType.cs} | 2 +- .../AwaitsInlineCandidateMatrix.cs} | 16 +++--- ...cMethodsOnGenericType.GenericContainer.cs} | 0 ...eCandidateMatrix.InlineCandidateMatrix.cs} | 2 +- 11 files changed, 44 insertions(+), 70 deletions(-) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{CrossModuleGenericLib.cs => MultiInlinerConsumer.GenericWrappers.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{MultiStepLibA.cs => MultiStepConsumer.Leaf.cs} (92%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{MultiStepLibB.cs => MultiStepConsumer.Mid.cs} (53%) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/{AsyncNoYield.cs => AsyncWithoutYield.cs} (94%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/{CompositeAsyncGenericTypesMain.cs => AwaitsAsyncMethodsOnGenericType.cs} (96%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/{CrossModuleInlining/AsyncInlineCallers.cs => RuntimeAsync/AwaitsInlineCandidateMatrix.cs} (64%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{AsyncGenericTypeLib.cs => AwaitsAsyncMethodsOnGenericType.GenericContainer.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/{CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs => RuntimeAsync/Dependencies/AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs} (97%) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiInlinerConsumer.GenericWrappers.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiInlinerConsumer.GenericWrappers.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepConsumer.Leaf.cs similarity index 92% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepConsumer.Leaf.cs index c47dd6ac274f28..21bb091444bbf1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepConsumer.Leaf.cs @@ -3,7 +3,7 @@ using System; using System.Runtime.CompilerServices; -public static class MultiStepLibA +public static class MultiStepLeaf { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetValue() => 42; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepConsumer.Mid.cs similarity index 53% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepConsumer.Mid.cs index cf8acd54d8bd9b..61fd8a2ca4a758 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepConsumer.Mid.cs @@ -1,13 +1,13 @@ // Second library for multi-step composite compilation. -// Compiled together with MultiStepLibA as a composite in step 1. +// Compiled together with MultiStepLeaf as a composite in step 1. using System; using System.Runtime.CompilerServices; -public static class MultiStepLibB +public static class MultiStepMid { [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetCompositeValue() => MultiStepLibA.GetValue() + 1; + public static int GetCompositeValue() => MultiStepLeaf.GetValue() + 1; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetCompositeLabel() => MultiStepLibA.GetLabel() + "_B"; + public static string GetCompositeLabel() => MultiStepLeaf.GetLabel() + "_B"; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs index ec1264f22f1380..9f17f969425937 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs @@ -1,5 +1,5 @@ // Test: Non-composite consumer of assemblies that were also compiled as composite. -// Step 1 compiles MultiStepLibA + MultiStepLibB as composite. +// Step 1 compiles MultiStepLeaf + MultiStepLibB as composite. // Step 2 compiles this assembly non-composite with --ref to LibA and --opt-cross-module. using System; using System.Runtime.CompilerServices; @@ -9,12 +9,12 @@ public static class MultiStepConsumer [MethodImpl(MethodImplOptions.NoInlining)] public static int GetValueFromLibA() { - return MultiStepLibA.GetValue(); + return MultiStepLeaf.GetValue(); } [MethodImpl(MethodImplOptions.NoInlining)] public static string GetLabelFromLibA() { - return MultiStepLibA.GetLabel(); + return MultiStepLeaf.GetLabel(); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index a90b8778e713e5..5cd1b23e3aed8a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -220,7 +220,7 @@ public void RuntimeAsyncContinuationLayout() AssemblyName = nameof(RuntimeAsyncContinuationLayout), SourceResourceNames = [ - "RuntimeAsync/AsyncWithContinuation.cs", + "RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -238,11 +238,11 @@ public void RuntimeAsyncContinuationLayout() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureObjectAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureMultipleRefsAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureObjectAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureMultipleRefsAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasResumptionStubFixup(reader, "CaptureObjectAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureArrayAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureRefAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureArrayAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasResumptionStubFixup(reader, "CaptureRefAcrossAwait", out diag), diag); } } @@ -293,7 +293,7 @@ public void RuntimeAsyncNoYield() AssemblyName = nameof(RuntimeAsyncNoYield), SourceResourceNames = [ - "RuntimeAsync/AsyncNoYield.cs", + "RuntimeAsync/AsyncWithoutYield.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -536,14 +536,14 @@ public void CompositeAsyncInliningMatrix() { var asyncInlineCandidatesLib = new CompiledAssembly { - AssemblyName = "AsyncInlineCandidatesLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs"], + AssemblyName = "InlineCandidateMatrix", + SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs"], Features = { RuntimeAsyncFeature }, }; var asyncInlineCallers = new CompiledAssembly { - AssemblyName = "AsyncInlineCallers", - SourceResourceNames = ["CrossModuleInlining/AsyncInlineCallers.cs"], + AssemblyName = "AwaitsInlineCandidateMatrix", + SourceResourceNames = ["RuntimeAsync/AwaitsInlineCandidateMatrix.cs"], Features = { RuntimeAsyncFeature }, References = [asyncInlineCandidatesLib] }; @@ -565,7 +565,7 @@ public void CompositeAsyncInliningMatrix() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "AsyncInlineCandidatesLib", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlineCandidateMatrix", out diag), diag); // Awaitless async candidates: should be inlined into their callers. Assert.True(R2RAssert.HasInlinedMethod(reader, "CallReturnTaskNoAwait", "ReturnTaskNoAwait", out diag), diag); @@ -663,20 +663,20 @@ public void CompositeAsyncGenericTypes() { var asyncGenericTypeLib = new CompiledAssembly { - AssemblyName = "AsyncGenericTypeLib", + AssemblyName = "AwaitsAsyncMethodsOnGenericType.GenericContainer", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs", + "RuntimeAsync/Dependencies/AwaitsAsyncMethodsOnGenericType.GenericContainer.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, }; var compositeAsyncGenericTypesMain = new CompiledAssembly { - AssemblyName = "CompositeAsyncGenericTypesMain", + AssemblyName = "AwaitsAsyncMethodsOnGenericType", SourceResourceNames = [ - "RuntimeAsync/CompositeAsyncGenericTypesMain.cs", + "RuntimeAsync/AwaitsAsyncMethodsOnGenericType.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -783,13 +783,13 @@ public void MultiStepCompositeAndNonComposite() { var libA = new CompiledAssembly { - AssemblyName = "MultiStepLibA", - SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepLibA.cs"], + AssemblyName = "MultiStepLeaf", + SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepConsumer.Leaf.cs"], }; var libB = new CompiledAssembly { - AssemblyName = "MultiStepLibB", - SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepLibB.cs"], + AssemblyName = "MultiStepMid", + SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepConsumer.Mid.cs"], References = [libA] }; var consumer = new CompiledAssembly @@ -812,7 +812,7 @@ public void MultiStepCompositeAndNonComposite() Validate = reader => { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "MultiStepLibA", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "MultiStepLeaf", out diag), diag); }, }, new("NonCompositeStep", @@ -828,7 +828,7 @@ public void MultiStepCompositeAndNonComposite() Validate = reader => { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "MultiStepLibA", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "MultiStepLeaf", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "GetValueFromLibA", "GetValue", out diag), diag); }, }, @@ -1134,8 +1134,8 @@ public void CrossModuleGenericMultiInliner() { var crossModuleGenericLib = new CompiledAssembly { - AssemblyName = "CrossModuleGenericLib", - SourceResourceNames = ["CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs"], + AssemblyName = "MultiInlinerConsumer.GenericWrappers", + SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiInlinerConsumer.GenericWrappers.cs"], }; var consumer = new CompiledAssembly { @@ -1164,7 +1164,7 @@ public void CrossModuleGenericMultiInliner() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "CrossModuleGenericLib", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "MultiInlinerConsumer.GenericWrappers", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInliningInfo(reader, out diag), diag); // Verify that GetValue has cross-module inliners from both GenericWrapperA and GenericWrapperB. diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs deleted file mode 100644 index 29a8fc879ec463..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Test: Async method that captures GC refs across await -// This forces the compiler to emit a ContinuationLayout fixup. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncWithContinuation -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CaptureObjectAcrossAwait() - { - object o = new object(); - string s = "hello"; - await Task.Yield(); - return s + o.GetHashCode(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CaptureMultipleRefsAcrossAwait() - { - int[] arr = new int[] { 1, 2, 3 }; - string text = "world"; - await Task.Yield(); - return arr[0] + text.Length; - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithoutYield.cs similarity index 94% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithoutYield.cs index 28ddb07949521c..8d2c1140a8e572 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithoutYield.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class AsyncNoYield +public static class AsyncWithoutYield { [MethodImpl(MethodImplOptions.NoInlining)] public static async Task AsyncButNoAwait() diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsAsyncMethodsOnGenericType.cs similarity index 96% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsAsyncMethodsOnGenericType.cs index 8dbd03c5de1fd1..5e6928aea5699b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsAsyncMethodsOnGenericType.cs @@ -11,7 +11,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class CompositeAsyncGenericTypesMain +public static class AwaitsAsyncMethodsOnGenericType { [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallGenericContainerInt() diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlineCandidateMatrix.cs similarity index 64% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlineCandidateMatrix.cs index da1e0dff49d4f8..0864a8a69d5aaf 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlineCandidateMatrix.cs @@ -1,44 +1,44 @@ -// Six call sites that each invoke one of the AsyncInlineCandidatesLib methods. +// Six call sites that each invoke one of the InlineCandidateMatrix methods. // Each caller is marked NoInlining so it stays put as the inliner; the test // then asserts which candidates get inlined into their respective callers. using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class AsyncInlineCallers +public static class AwaitsInlineCandidateMatrix { [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskNoAwait() { - await AsyncInlineCandidatesLib.ReturnTaskNoAwait(); + await InlineCandidateMatrix.ReturnTaskNoAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskPrimitiveNoAwait() { - return await AsyncInlineCandidatesLib.ReturnTaskPrimitiveNoAwait(); + return await InlineCandidateMatrix.ReturnTaskPrimitiveNoAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskClassNoAwait() { - return await AsyncInlineCandidatesLib.ReturnTaskClassNoAwait(); + return await InlineCandidateMatrix.ReturnTaskClassNoAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskWithAwait() { - await AsyncInlineCandidatesLib.ReturnTaskWithAwait(); + await InlineCandidateMatrix.ReturnTaskWithAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskPrimitiveWithAwait() { - return await AsyncInlineCandidatesLib.ReturnTaskPrimitiveWithAwait(); + return await InlineCandidateMatrix.ReturnTaskPrimitiveWithAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskClassWithAwait() { - return await AsyncInlineCandidatesLib.ReturnTaskClassWithAwait(); + return await InlineCandidateMatrix.ReturnTaskClassWithAwait(); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsAsyncMethodsOnGenericType.GenericContainer.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsAsyncMethodsOnGenericType.GenericContainer.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs similarity index 97% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs index d4d40a11218fe4..7a201a6798a42b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs @@ -7,7 +7,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class AsyncInlineCandidatesLib +public static class InlineCandidateMatrix { // --- Awaitless variants: should be inlinable --- From 2203bd7c2878c34b8997ba78f8bd9e08959ac688 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:38:12 -0700 Subject: [PATCH 54/74] Rename R2R test deps to .cs; merge InlinableAsyncMethods into InlineCandidateMatrix Dep files now use just the class name so they can be shared across tests without being tied to a particular consumer. Renames (no behavior change): * BasicInlining.SyncInlinableMethods.cs -> SyncInlinableMethods.cs * CompositeBasic.SyncTypeAndMethod.cs -> SyncTypeAndMethod.cs * MultiInlinerConsumer.GenericWrappers.cs -> GenericWrappers.cs * MultiStepConsumer.Leaf.cs -> MultiStepLeaf.cs * MultiStepConsumer.Mid.cs -> MultiStepMid.cs * TransitiveReferences.InlinableLeafCallers.cs -> InlinableLeafCallers.cs * AwaitsAsyncMethodsOnGenericType.GenericContainer.cs -> GenericContainer.cs * AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs -> InlineCandidateMatrix.cs * AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs -> LocalsCapturedAcrossAwait.cs * AwaitsThroughInterface.InterfaceAndImpls.cs -> AsyncInterfaceAndImpls.cs * AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs -> InlinableAsyncLeafCallers.cs Merge: AwaitsInlinableAsync.InlinableAsyncMethods.cs (class InlinableAsyncMethods with GetValueAsync/GetStringAsync/GetValueSync) is deleted. The two async methods were functional duplicates of InlineCandidateMatrix's ReturnTaskPrimitiveWithAwait / ReturnTaskClassWithAwait; GetValueSync is added to InlineCandidateMatrix. AwaitsInlinableAsync.cs now calls InlineCandidateMatrix directly, and the validators that asserted on 'GetValueAsync' are updated to 'ReturnTaskPrimitiveWithAwait'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ....GenericWrappers.cs => GenericWrappers.cs} | 0 ...LeafCallers.cs => InlinableLeafCallers.cs} | 0 ...iStepConsumer.Leaf.cs => MultiStepLeaf.cs} | 0 ...ltiStepConsumer.Mid.cs => MultiStepMid.cs} | 0 ...ableMethods.cs => SyncInlinableMethods.cs} | 0 ...cTypeAndMethod.cs => SyncTypeAndMethod.cs} | 0 .../TestCases/R2RTestSuites.cs | 66 +++++++++---------- .../RuntimeAsync/AwaitsInlinableAsync.cs | 13 ++-- ...eAndImpls.cs => AsyncInterfaceAndImpls.cs} | 0 ...itsInlinableAsync.InlinableAsyncMethods.cs | 25 ------- ...enericContainer.cs => GenericContainer.cs} | 0 ...allers.cs => InlinableAsyncLeafCallers.cs} | 0 ...dateMatrix.cs => InlineCandidateMatrix.cs} | 5 ++ ...sAwait.cs => LocalsCapturedAcrossAwait.cs} | 0 14 files changed, 44 insertions(+), 65 deletions(-) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{MultiInlinerConsumer.GenericWrappers.cs => GenericWrappers.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{TransitiveReferences.InlinableLeafCallers.cs => InlinableLeafCallers.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{MultiStepConsumer.Leaf.cs => MultiStepLeaf.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{MultiStepConsumer.Mid.cs => MultiStepMid.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{BasicInlining.SyncInlinableMethods.cs => SyncInlinableMethods.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{CompositeBasic.SyncTypeAndMethod.cs => SyncTypeAndMethod.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{AwaitsThroughInterface.InterfaceAndImpls.cs => AsyncInterfaceAndImpls.cs} (100%) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{AwaitsAsyncMethodsOnGenericType.GenericContainer.cs => GenericContainer.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs => InlinableAsyncLeafCallers.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs => InlineCandidateMatrix.cs} (90%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs => LocalsCapturedAcrossAwait.cs} (100%) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiInlinerConsumer.GenericWrappers.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/GenericWrappers.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiInlinerConsumer.GenericWrappers.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/GenericWrappers.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/TransitiveReferences.InlinableLeafCallers.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlinableLeafCallers.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/TransitiveReferences.InlinableLeafCallers.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlinableLeafCallers.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepConsumer.Leaf.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLeaf.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepConsumer.Leaf.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLeaf.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepConsumer.Mid.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepMid.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepConsumer.Mid.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepMid.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncInlinableMethods.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncInlinableMethods.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeBasic.SyncTypeAndMethod.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncTypeAndMethod.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeBasic.SyncTypeAndMethod.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncTypeAndMethod.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 5cd1b23e3aed8a..f885a3f45268d7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -29,7 +29,7 @@ public void BasicCrossModuleInlining() var syncInlinableMethods = new CompiledAssembly { AssemblyName = "SyncInlinableMethods", - SourceResourceNames = ["CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs"], + SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncInlinableMethods.cs"], }; var basicCrossModuleInlining = new CompiledAssembly { @@ -67,7 +67,7 @@ public void TransitiveReferences() var inlinableLeafCallers = new CompiledAssembly() { AssemblyName = "InlinableLeafCallers", - SourceResourceNames = ["CrossModuleInlining/Dependencies/TransitiveReferences.InlinableLeafCallers.cs"], + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlinableLeafCallers.cs"], References = [syncLeafMethods] }; var transitiveReferences = new CompiledAssembly() @@ -104,8 +104,8 @@ public void AsyncCrossModuleInlining() { var inlinableAsyncMethods = new CompiledAssembly { - AssemblyName = "InlinableAsyncMethods", - SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs"], + AssemblyName = "InlineCandidateMatrix", + SourceResourceNames = ["RuntimeAsync/Dependencies/InlineCandidateMatrix.cs"], }; var awaitsInlinableAsync = new CompiledAssembly { @@ -134,8 +134,8 @@ public void AsyncCrossModuleInlining() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlinableAsyncMethods", out diag), diag); - Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "CallGetValueAsync", "GetValueAsync", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlineCandidateMatrix", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "CallGetValueAsync", "ReturnTaskPrimitiveWithAwait", out diag), diag); } } @@ -145,7 +145,7 @@ public void CompositeBasic() var compositeLib = new CompiledAssembly { AssemblyName = "SyncTypeAndMethod", - SourceResourceNames = ["CrossModuleInlining/Dependencies/CompositeBasic.SyncTypeAndMethod.cs"], + SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncTypeAndMethod.cs"], }; var compositeBasic = new CompiledAssembly { @@ -220,7 +220,7 @@ public void RuntimeAsyncContinuationLayout() AssemblyName = nameof(RuntimeAsyncContinuationLayout), SourceResourceNames = [ - "RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs", + "RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -259,7 +259,7 @@ public void RuntimeAsyncDevirtualize() SourceResourceNames = [ "RuntimeAsync/AwaitsThroughInterface.cs", - "RuntimeAsync/Dependencies/AwaitsThroughInterface.InterfaceAndImpls.cs", + "RuntimeAsync/Dependencies/AsyncInterfaceAndImpls.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -325,10 +325,10 @@ public void RuntimeAsyncCrossModule() { var inlinableAsyncMethods = new CompiledAssembly { - AssemblyName = "InlinableAsyncMethods", + AssemblyName = "InlineCandidateMatrix", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs", + "RuntimeAsync/Dependencies/InlineCandidateMatrix.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -365,7 +365,7 @@ public void RuntimeAsyncCrossModule() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlinableAsyncMethods", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlineCandidateMatrix", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "CallGetValueAsync", out diag), diag); } } @@ -385,7 +385,7 @@ public void CompositeCrossModuleInlining() var syncInlinableMethods = new CompiledAssembly { AssemblyName = "SyncInlinableMethods", - SourceResourceNames = ["CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs"], + SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncInlinableMethods.cs"], }; var compositeMain = new CompiledAssembly { @@ -435,7 +435,7 @@ public void CompositeDoesNotProduceCrossModuleInliningInfo() var syncInlinableMethods = new CompiledAssembly { AssemblyName = "SyncInlinableMethods", - SourceResourceNames = ["CrossModuleInlining/Dependencies/BasicInlining.SyncInlinableMethods.cs"], + SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncInlinableMethods.cs"], }; var compositeMain = new CompiledAssembly { @@ -479,8 +479,8 @@ public void CompositeAsync() { var inlinableAsyncMethods = new CompiledAssembly { - AssemblyName = "InlinableAsyncMethods", - SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs"], + AssemblyName = "InlineCandidateMatrix", + SourceResourceNames = ["RuntimeAsync/Dependencies/InlineCandidateMatrix.cs"], Features = { RuntimeAsyncFeature }, }; var awaitsInlinableAsync = new CompiledAssembly @@ -508,15 +508,15 @@ public void CompositeAsync() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlinableAsyncMethods", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlineCandidateMatrix", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "CallGetValueAsync", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "GetValueAsync", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "ReturnTaskPrimitiveWithAwait", out diag), diag); } } /// /// The full intersection: composite + runtime-async + cross-module inlining. - /// Async methods from InlinableAsyncMethods are inlined into AwaitsInlinableAsync + /// Async methods from InlineCandidateMatrix are inlined into AwaitsInlinableAsync /// within a composite image, exercising MutableModule token encoding for /// cross-module async continuation layouts. /// @@ -537,7 +537,7 @@ public void CompositeAsyncInliningMatrix() var asyncInlineCandidatesLib = new CompiledAssembly { AssemblyName = "InlineCandidateMatrix", - SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs"], + SourceResourceNames = ["RuntimeAsync/Dependencies/InlineCandidateMatrix.cs"], Features = { RuntimeAsyncFeature }, }; var asyncInlineCallers = new CompiledAssembly @@ -593,7 +593,7 @@ public void CompositeAsyncContinuationAndResume() AssemblyName = "LocalsCapturedAcrossAwait", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs", + "RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -666,7 +666,7 @@ public void CompositeAsyncGenericTypes() AssemblyName = "AwaitsAsyncMethodsOnGenericType.GenericContainer", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AwaitsAsyncMethodsOnGenericType.GenericContainer.cs", + "RuntimeAsync/Dependencies/GenericContainer.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -731,7 +731,7 @@ public void AsyncCrossModuleContinuation() AssemblyName = "LocalsCapturedAcrossAwait", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs", + "RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -784,12 +784,12 @@ public void MultiStepCompositeAndNonComposite() var libA = new CompiledAssembly { AssemblyName = "MultiStepLeaf", - SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepConsumer.Leaf.cs"], + SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepLeaf.cs"], }; var libB = new CompiledAssembly { AssemblyName = "MultiStepMid", - SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepConsumer.Mid.cs"], + SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepMid.cs"], References = [libA] }; var consumer = new CompiledAssembly @@ -849,7 +849,7 @@ public void CompositeAsyncDevirtualize() var interfaceAndImpls = new CompiledAssembly { AssemblyName = "InterfaceAndImpls", - SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsThroughInterface.InterfaceAndImpls.cs"], + SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncInterfaceAndImpls.cs"], Features = { RuntimeAsyncFeature }, }; var awaitsThroughInterface = new CompiledAssembly @@ -901,7 +901,7 @@ public void CompositeTransitive() var inlinableLeafCallers = new CompiledAssembly { AssemblyName = "InlinableLeafCallers", - SourceResourceNames = ["CrossModuleInlining/Dependencies/TransitiveReferences.InlinableLeafCallers.cs"], + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlinableLeafCallers.cs"], References = [syncLeafMethods] }; var compositeTransitiveMain = new CompiledAssembly @@ -951,7 +951,7 @@ public void AsyncCrossModuleTransitive() AssemblyName = "InlinableAsyncLeafCallers", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs", + "RuntimeAsync/Dependencies/InlinableAsyncLeafCallers.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -1012,7 +1012,7 @@ public void CompositeAsyncTransitive() AssemblyName = "InlinableAsyncLeafCallers", SourceResourceNames = [ - "RuntimeAsync/Dependencies/AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs", + "RuntimeAsync/Dependencies/InlinableAsyncLeafCallers.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -1064,13 +1064,13 @@ public void MultiStepCompositeAndNonCompositeAsync() var locals = new CompiledAssembly { AssemblyName = "LocalsCapturedAcrossAwait", - SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs"], + SourceResourceNames = ["RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs"], Features = { RuntimeAsyncFeature }, }; var inlinableAsyncMethods = new CompiledAssembly { - AssemblyName = "InlinableAsyncMethods", - SourceResourceNames = ["RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs"], + AssemblyName = "InlineCandidateMatrix", + SourceResourceNames = ["RuntimeAsync/Dependencies/InlineCandidateMatrix.cs"], Features = { RuntimeAsyncFeature }, }; var asyncConsumer = new CompiledAssembly @@ -1135,7 +1135,7 @@ public void CrossModuleGenericMultiInliner() var crossModuleGenericLib = new CompiledAssembly { AssemblyName = "MultiInlinerConsumer.GenericWrappers", - SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiInlinerConsumer.GenericWrappers.cs"], + SourceResourceNames = ["CrossModuleInlining/Dependencies/GenericWrappers.cs"], }; var consumer = new CompiledAssembly { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs index ca87d90cb855f1..8d8b24e45bcfc3 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs @@ -1,7 +1,6 @@ -// Awaits each method in InlinableAsyncMethods so that callers see both -// cross-module inlining opportunities and composite-mode async variant -// emission, depending on how the tests wire the compilation. -using System; +// Awaits a small subset of InlineCandidateMatrix methods (one Task, one +// Task, one sync) so that cross-module and composite tests can observe +// async-variant emission and cross-module inlining of inlinable async methods. using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -10,18 +9,18 @@ public static class AwaitsInlinableAsync [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallGetValueAsync() { - return await InlinableAsyncMethods.GetValueAsync(); + return await InlineCandidateMatrix.ReturnTaskPrimitiveWithAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallGetStringAsync() { - return await InlinableAsyncMethods.GetStringAsync(); + return await InlineCandidateMatrix.ReturnTaskClassWithAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static int CallGetValueSync() { - return InlinableAsyncMethods.GetValueSync(); + return InlineCandidateMatrix.GetValueSync(); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsThroughInterface.InterfaceAndImpls.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceAndImpls.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsThroughInterface.InterfaceAndImpls.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceAndImpls.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs deleted file mode 100644 index ca4e53889e480d..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlinableAsync.InlinableAsyncMethods.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Inlinable runtime-async methods used to exercise cross-module inlining -// and composite-mode emission of async variants. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class InlinableAsyncMethods -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetValueAsync() - { - await Task.Yield(); - return 42; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task GetStringAsync() - { - await Task.Yield(); - return "hello_from_async"; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetValueSync() => 99; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsAsyncMethodsOnGenericType.GenericContainer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/GenericContainer.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsAsyncMethodsOnGenericType.GenericContainer.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/GenericContainer.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlinableAsyncLeafCallers.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsTransitiveAsync.InlinableAsyncLeafCallers.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlinableAsyncLeafCallers.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlineCandidateMatrix.cs similarity index 90% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlineCandidateMatrix.cs index 7a201a6798a42b..1d662734984d02 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsInlineCandidateMatrix.InlineCandidateMatrix.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlineCandidateMatrix.cs @@ -43,4 +43,9 @@ public static async Task ReturnTaskClassWithAwait() await Task.Yield(); return "with_await"; } + + // --- Sync helper used by cross-module inlining tests --- + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetValueSync() => 99; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AwaitsLocalsCapturedAcrossAwait.LocalsCapturedAcrossAwait.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs From fdcb6c232d953f82ab09de40668f8005e200b93a Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:18:23 -0700 Subject: [PATCH 55/74] Revert R2R test fixture dedup/rename to keep follow-up PR isolated Reverts the 6 commits between 0f5280dda3d and 2203bd7c287 that renamed and deduplicated test fixtures under ILCompiler.ReadyToRun.Tests/TestCases/. Those changes will land in a follow-up PR (branch r2r-test-fixture-cleanup) so this PR stays focused on the composite-async runtime work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AsyncInlineCallers.cs} | 16 +- .../CrossModuleInlining/AsyncMethods.cs | 27 ++ .../CrossModuleInlining/BasicInlining.cs | 6 +- .../CrossModuleInlining/CompositeAsync.cs | 26 ++ .../CrossModuleInlining/CompositeBasic.cs | 4 +- .../Dependencies/AsyncCompositeLib.cs | 25 ++ .../Dependencies/AsyncInlineCandidatesLib.cs} | 7 +- .../Dependencies/AsyncInlineableLib.cs | 15 + .../{SyncTypeAndMethod.cs => CompositeLib.cs} | 2 +- ...icWrappers.cs => CrossModuleGenericLib.cs} | 0 .../{SyncLeafMethods.cs => ExternalLib.cs} | 5 +- .../Dependencies/InlinableLeafCallers.cs | 17 - ...ncInlinableMethods.cs => InlineableLib.cs} | 4 +- .../Dependencies/InlineableLibTransitive.cs | 14 + .../{MultiStepLeaf.cs => MultiStepLibA.cs} | 2 +- .../{MultiStepMid.cs => MultiStepLibB.cs} | 8 +- .../CrossModuleInlining/MultiStepConsumer.cs | 6 +- .../TransitiveReferences.cs | 12 +- .../TestCases/R2RTestSuites.cs | 359 +++++++++--------- .../RuntimeAsync/AsyncCrossModule.cs | 28 ++ .../AsyncCrossModuleContinuation.cs | 21 + .../RuntimeAsync/AsyncDevirtualize.cs | 56 +++ .../{AsyncWithoutYield.cs => AsyncNoYield.cs} | 2 +- .../RuntimeAsync/AsyncTransitiveMain.cs | 21 + .../RuntimeAsync/AsyncWithContinuation.cs | 26 ++ .../RuntimeAsync/AwaitsInlinableAsync.cs | 26 -- .../AwaitsLocalsCapturedAcrossAwait.cs | 29 -- .../RuntimeAsync/AwaitsThroughInterface.cs | 28 -- .../RuntimeAsync/AwaitsTransitiveAsync.cs | 20 - .../CompositeAsyncContinuationMain.cs | 30 ++ .../RuntimeAsync/CompositeAsyncDevirtMain.cs | 22 ++ ...e.cs => CompositeAsyncGenericTypesMain.cs} | 2 +- .../Dependencies/AsyncCompositeContLib.cs | 28 ++ .../RuntimeAsync/Dependencies/AsyncDepLib.cs | 28 ++ ...ossAwait.cs => AsyncDepLibContinuation.cs} | 14 +- .../Dependencies/AsyncExternalLib.cs | 13 + ...ricContainer.cs => AsyncGenericTypeLib.cs} | 0 ...erfaceAndImpls.cs => AsyncInterfaceLib.cs} | 13 +- ...ncLeafCallers.cs => AsyncTransitiveLib.cs} | 10 +- 39 files changed, 611 insertions(+), 361 deletions(-) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/{RuntimeAsync/AwaitsInlineCandidateMatrix.cs => CrossModuleInlining/AsyncInlineCallers.cs} (64%) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/{RuntimeAsync/Dependencies/InlineCandidateMatrix.cs => CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs} (88%) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{SyncTypeAndMethod.cs => CompositeLib.cs} (81%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{GenericWrappers.cs => CrossModuleGenericLib.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{SyncLeafMethods.cs => ExternalLib.cs} (58%) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlinableLeafCallers.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{SyncInlinableMethods.cs => InlineableLib.cs} (73%) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{MultiStepLeaf.cs => MultiStepLibA.cs} (92%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/{MultiStepMid.cs => MultiStepLibB.cs} (53%) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/{AsyncWithoutYield.cs => AsyncNoYield.cs} (94%) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsThroughInterface.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsTransitiveAsync.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/{AwaitsAsyncMethodsOnGenericType.cs => CompositeAsyncGenericTypesMain.cs} (96%) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{LocalsCapturedAcrossAwait.cs => AsyncDepLibContinuation.cs} (54%) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{GenericContainer.cs => AsyncGenericTypeLib.cs} (100%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{AsyncInterfaceAndImpls.cs => AsyncInterfaceLib.cs} (56%) rename src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/{InlinableAsyncLeafCallers.cs => AsyncTransitiveLib.cs} (55%) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlineCandidateMatrix.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs similarity index 64% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlineCandidateMatrix.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs index 0864a8a69d5aaf..da1e0dff49d4f8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlineCandidateMatrix.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs @@ -1,44 +1,44 @@ -// Six call sites that each invoke one of the InlineCandidateMatrix methods. +// Six call sites that each invoke one of the AsyncInlineCandidatesLib methods. // Each caller is marked NoInlining so it stays put as the inliner; the test // then asserts which candidates get inlined into their respective callers. using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class AwaitsInlineCandidateMatrix +public static class AsyncInlineCallers { [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskNoAwait() { - await InlineCandidateMatrix.ReturnTaskNoAwait(); + await AsyncInlineCandidatesLib.ReturnTaskNoAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskPrimitiveNoAwait() { - return await InlineCandidateMatrix.ReturnTaskPrimitiveNoAwait(); + return await AsyncInlineCandidatesLib.ReturnTaskPrimitiveNoAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskClassNoAwait() { - return await InlineCandidateMatrix.ReturnTaskClassNoAwait(); + return await AsyncInlineCandidatesLib.ReturnTaskClassNoAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskWithAwait() { - await InlineCandidateMatrix.ReturnTaskWithAwait(); + await AsyncInlineCandidatesLib.ReturnTaskWithAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskPrimitiveWithAwait() { - return await InlineCandidateMatrix.ReturnTaskPrimitiveWithAwait(); + return await AsyncInlineCandidatesLib.ReturnTaskPrimitiveWithAwait(); } [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallReturnTaskClassWithAwait() { - return await InlineCandidateMatrix.ReturnTaskClassWithAwait(); + return await AsyncInlineCandidatesLib.ReturnTaskClassWithAwait(); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs new file mode 100644 index 00000000000000..fdf1b159130b95 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs @@ -0,0 +1,27 @@ +// Test: Cross-module inlining of async methods +// Validates that async methods from AsyncInlineableLib are cross-module +// inlined into this assembly with CHECK_IL_BODY fixups. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncMethods +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task TestAsyncInline() + { + return await AsyncInlineableLib.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task TestAsyncStringInline() + { + return await AsyncInlineableLib.GetStringAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int TestSyncFromAsyncLib() + { + return AsyncInlineableLib.GetValueSync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs index 0a76a9780b6d05..ba301272868c25 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs @@ -9,18 +9,18 @@ public static class BasicInlining [MethodImpl(MethodImplOptions.NoInlining)] public static int TestGetValue() { - return SyncInlinableMethods.GetValue(); + return InlineableLib.GetValue(); } [MethodImpl(MethodImplOptions.NoInlining)] public static string TestGetString() { - return SyncInlinableMethods.GetString(); + return InlineableLib.GetString(); } [MethodImpl(MethodImplOptions.NoInlining)] public static int TestAdd() { - return SyncInlinableMethods.Add(10, 32); + return InlineableLib.Add(10, 32); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs new file mode 100644 index 00000000000000..7e19cc7eea200c --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs @@ -0,0 +1,26 @@ +// Test: Composite mode with runtime-async methods across assemblies. +// Validates that async methods produce [ASYNC] variants in composite output. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class CompositeAsyncMain +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCompositeAsync() + { + return await AsyncCompositeLib.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCompositeStringAsync() + { + return await AsyncCompositeLib.GetStringAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CallCompositeSync() + { + return AsyncCompositeLib.GetValueSync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs index abbd5b2131bde2..0ba24d5563555c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs @@ -9,12 +9,12 @@ public static class CompositeBasic [MethodImpl(MethodImplOptions.NoInlining)] public static int TestCompositeCall() { - return SyncTypeAndMethod.GetCompositeValue(); + return CompositeLib.GetCompositeValue(); } [MethodImpl(MethodImplOptions.NoInlining)] public static object TestCompositeTypeCreation() { - return new SyncTypeAndMethod.CompositeType(); + return new CompositeLib.CompositeType(); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs new file mode 100644 index 00000000000000..bece15b74b11b6 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs @@ -0,0 +1,25 @@ +// Dependency library for composite async tests. +// Contains runtime-async methods called from another assembly in composite mode. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncCompositeLib +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetValueAsync() + { + await Task.Yield(); + return 42; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetStringAsync() + { + await Task.Yield(); + return "composite_async"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetValueSync() => 99; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlineCandidateMatrix.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs similarity index 88% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlineCandidateMatrix.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs index 1d662734984d02..d4d40a11218fe4 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlineCandidateMatrix.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs @@ -7,7 +7,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class InlineCandidateMatrix +public static class AsyncInlineCandidatesLib { // --- Awaitless variants: should be inlinable --- @@ -43,9 +43,4 @@ public static async Task ReturnTaskClassWithAwait() await Task.Yield(); return "with_await"; } - - // --- Sync helper used by cross-module inlining tests --- - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetValueSync() => 99; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs new file mode 100644 index 00000000000000..153804e6279fc1 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncInlineableLib +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetValueAsync() => 42; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetStringAsync() => "Hello from async"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetValueSync() => 42; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncTypeAndMethod.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs similarity index 81% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncTypeAndMethod.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs index 2ed46a18a34986..2dc5db2de38bcc 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncTypeAndMethod.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs @@ -1,6 +1,6 @@ using System; -public static class SyncTypeAndMethod +public static class CompositeLib { public static int GetCompositeValue() => 100; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/GenericWrappers.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/GenericWrappers.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncLeafMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs similarity index 58% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncLeafMethods.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs index dffb22616202f8..d56f2880564a13 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncLeafMethods.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs @@ -1,15 +1,12 @@ -// Leaf data types and values used by other cross-module fixtures to -// exercise transitive references. using System; -public static class SyncLeafMethods +public static class ExternalLib { public static int ExternalValue => 99; public class ExternalType { public int Value { get; set; } - public string Label { get; set; } = "external"; } public class Outer diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlinableLeafCallers.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlinableLeafCallers.cs deleted file mode 100644 index 79e33ab247bb9c..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlinableLeafCallers.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Inlinable helpers that forward sync calls through to SyncLeafMethods. -// Used to exercise transitive cross-module inlining. -using System; -using System.Runtime.CompilerServices; - -public static class InlinableLeafCallers -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetExternalValue() => SyncLeafMethods.ExternalValue; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetNestedValue() => SyncLeafMethods.Outer.Inner.NestedValue; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static SyncLeafMethods.ExternalType CreateExternal() => - new SyncLeafMethods.ExternalType { Value = 42 }; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncInlinableMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs similarity index 73% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncInlinableMethods.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs index 51944a1ce2fde4..a799cfed7282e8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/SyncInlinableMethods.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs @@ -1,13 +1,13 @@ using System; using System.Runtime.CompilerServices; -public static class SyncInlinableMethods +public static class InlineableLib { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetValue() => 42; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetString() => "Hello from SyncInlinableMethods"; + public static string GetString() => "Hello from InlineableLib"; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Add(int a, int b) => a + b; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs new file mode 100644 index 00000000000000..15fd29dda19d4b --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.CompilerServices; + +public static class InlineableLibTransitive +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetExternalValue() => ExternalLib.ExternalValue; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetNestedValue() => ExternalLib.Outer.Inner.NestedValue; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExternalLib.ExternalType CreateExternal() => new ExternalLib.ExternalType { Value = 42 }; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLeaf.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs similarity index 92% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLeaf.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs index 21bb091444bbf1..c47dd6ac274f28 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLeaf.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs @@ -3,7 +3,7 @@ using System; using System.Runtime.CompilerServices; -public static class MultiStepLeaf +public static class MultiStepLibA { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetValue() => 42; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepMid.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs similarity index 53% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepMid.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs index 61fd8a2ca4a758..cf8acd54d8bd9b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepMid.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs @@ -1,13 +1,13 @@ // Second library for multi-step composite compilation. -// Compiled together with MultiStepLeaf as a composite in step 1. +// Compiled together with MultiStepLibA as a composite in step 1. using System; using System.Runtime.CompilerServices; -public static class MultiStepMid +public static class MultiStepLibB { [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetCompositeValue() => MultiStepLeaf.GetValue() + 1; + public static int GetCompositeValue() => MultiStepLibA.GetValue() + 1; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetCompositeLabel() => MultiStepLeaf.GetLabel() + "_B"; + public static string GetCompositeLabel() => MultiStepLibA.GetLabel() + "_B"; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs index 9f17f969425937..ec1264f22f1380 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs @@ -1,5 +1,5 @@ // Test: Non-composite consumer of assemblies that were also compiled as composite. -// Step 1 compiles MultiStepLeaf + MultiStepLibB as composite. +// Step 1 compiles MultiStepLibA + MultiStepLibB as composite. // Step 2 compiles this assembly non-composite with --ref to LibA and --opt-cross-module. using System; using System.Runtime.CompilerServices; @@ -9,12 +9,12 @@ public static class MultiStepConsumer [MethodImpl(MethodImplOptions.NoInlining)] public static int GetValueFromLibA() { - return MultiStepLeaf.GetValue(); + return MultiStepLibA.GetValue(); } [MethodImpl(MethodImplOptions.NoInlining)] public static string GetLabelFromLibA() { - return MultiStepLeaf.GetLabel(); + return MultiStepLibA.GetLabel(); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs index 2d9de073226274..25bac820fc3002 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs @@ -1,6 +1,6 @@ -// Calls inlinable helpers in another module that transitively touch types -// and members from a further module, so the consumer's R2R image must -// carry tokens for both the mid-layer and leaf modules. +// Test: Transitive cross-module references +// Validates that when InlineableLibTransitive is inlined, its references to ExternalLib +// are properly encoded in the R2R image (requiring tokens for both libraries). using System; using System.Runtime.CompilerServices; @@ -9,18 +9,18 @@ public static class TransitiveReferences [MethodImpl(MethodImplOptions.NoInlining)] public static int TestTransitiveValue() { - return InlinableLeafCallers.GetExternalValue(); + return InlineableLibTransitive.GetExternalValue(); } [MethodImpl(MethodImplOptions.NoInlining)] public static int TestNestedTypeAccess() { - return InlinableLeafCallers.GetNestedValue(); + return InlineableLibTransitive.GetNestedValue(); } [MethodImpl(MethodImplOptions.NoInlining)] public static object TestTransitiveTypeCreation() { - return InlinableLeafCallers.CreateExternal(); + return InlineableLibTransitive.CreateExternal(); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index f885a3f45268d7..1816bdd4511027 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -26,30 +26,30 @@ public R2RTestSuites(ITestOutputHelper output) [Fact] public void BasicCrossModuleInlining() { - var syncInlinableMethods = new CompiledAssembly + var inlineableLib = new CompiledAssembly { - AssemblyName = "SyncInlinableMethods", - SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncInlinableMethods.cs"], + AssemblyName = "InlineableLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], }; var basicCrossModuleInlining = new CompiledAssembly { AssemblyName = "BasicCrossModuleInlining", SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], - References = [syncInlinableMethods] + References = [inlineableLib] }; - var cgSyncInlinableMethods = new CrossgenAssembly(syncInlinableMethods){ Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization] }; + var cgInlineableLib = new CrossgenAssembly(inlineableLib){ Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization] }; var cgBasicCrossModuleInlining = new CrossgenAssembly(basicCrossModuleInlining); new R2RTestRunner(_output).Run(new R2RTestCase( nameof(BasicCrossModuleInlining), - [new CrossgenCompilation(basicCrossModuleInlining.AssemblyName, [cgSyncInlinableMethods, cgBasicCrossModuleInlining]) { Validate = Validate }]) + [new CrossgenCompilation(basicCrossModuleInlining.AssemblyName, [cgInlineableLib, cgBasicCrossModuleInlining]) { Validate = Validate }]) ); static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "SyncInlinableMethods", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLib", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetValue", "GetValue", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetString", "GetString", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInliningInfo(reader, out diag), diag); @@ -59,29 +59,29 @@ static void Validate(ReadyToRunReader reader) [Fact] public void TransitiveReferences() { - var syncLeafMethods = new CompiledAssembly() + var externalLib = new CompiledAssembly() { - AssemblyName = "SyncLeafMethods", - SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncLeafMethods.cs"], + AssemblyName = "ExternalLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/ExternalLib.cs"], }; - var inlinableLeafCallers = new CompiledAssembly() + var inlineableLibTransitive = new CompiledAssembly() { - AssemblyName = "InlinableLeafCallers", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlinableLeafCallers.cs"], - References = [syncLeafMethods] + AssemblyName = "InlineableLibTransitive", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLibTransitive.cs"], + References = [externalLib] }; var transitiveReferences = new CompiledAssembly() { AssemblyName = "TransitiveReferences", SourceResourceNames = ["CrossModuleInlining/TransitiveReferences.cs"], - References = [inlinableLeafCallers, syncLeafMethods] + References = [inlineableLibTransitive, externalLib] }; new R2RTestRunner(_output).Run(new R2RTestCase(nameof(TransitiveReferences), [ new("TransitiveReferences", [ new CrossgenAssembly(transitiveReferences), - new CrossgenAssembly(syncLeafMethods) { Kind = Crossgen2InputKind.Reference }, - new CrossgenAssembly(inlinableLeafCallers) + new CrossgenAssembly(externalLib) { Kind = Crossgen2InputKind.Reference }, + new CrossgenAssembly(inlineableLibTransitive) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -91,8 +91,8 @@ public void TransitiveReferences() Validate = reader => { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlinableLeafCallers", out diag), diag); - Assert.True(R2RAssert.HasManifestRef(reader, "SyncLeafMethods", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLibTransitive", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "ExternalLib", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestTransitiveValue", "GetExternalValue", out diag), diag); }, }, @@ -102,16 +102,16 @@ public void TransitiveReferences() [Fact] public void AsyncCrossModuleInlining() { - var inlinableAsyncMethods = new CompiledAssembly + var asyncInlineableLib = new CompiledAssembly { - AssemblyName = "InlineCandidateMatrix", - SourceResourceNames = ["RuntimeAsync/Dependencies/InlineCandidateMatrix.cs"], + AssemblyName = "AsyncInlineableLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncInlineableLib.cs"], }; - var awaitsInlinableAsync = new CompiledAssembly + var asyncCrossModuleInlining = new CompiledAssembly { AssemblyName = nameof(AsyncCrossModuleInlining), - SourceResourceNames = ["RuntimeAsync/AwaitsInlinableAsync.cs"], - References = [inlinableAsyncMethods] + SourceResourceNames = ["CrossModuleInlining/AsyncMethods.cs"], + References = [asyncInlineableLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -119,8 +119,8 @@ public void AsyncCrossModuleInlining() [ new(nameof(AsyncCrossModuleInlining), [ - new CrossgenAssembly(awaitsInlinableAsync), - new CrossgenAssembly(inlinableAsyncMethods) + new CrossgenAssembly(asyncCrossModuleInlining), + new CrossgenAssembly(asyncInlineableLib) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -134,8 +134,8 @@ public void AsyncCrossModuleInlining() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlineCandidateMatrix", out diag), diag); - Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "CallGetValueAsync", "ReturnTaskPrimitiveWithAwait", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncInlineableLib", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestAsyncInline", "GetValueAsync", out diag), diag); } } @@ -144,8 +144,8 @@ public void CompositeBasic() { var compositeLib = new CompiledAssembly { - AssemblyName = "SyncTypeAndMethod", - SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncTypeAndMethod.cs"], + AssemblyName = "CompositeLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/CompositeLib.cs"], }; var compositeBasic = new CompiledAssembly { @@ -171,7 +171,7 @@ public void CompositeBasic() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "SyncTypeAndMethod", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "CompositeLib", out diag), diag); } } @@ -220,7 +220,7 @@ public void RuntimeAsyncContinuationLayout() AssemblyName = nameof(RuntimeAsyncContinuationLayout), SourceResourceNames = [ - "RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs", + "RuntimeAsync/AsyncWithContinuation.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -238,11 +238,11 @@ public void RuntimeAsyncContinuationLayout() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureArrayAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureRefAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureArrayAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasResumptionStubFixup(reader, "CaptureRefAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureObjectAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureMultipleRefsAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureObjectAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureMultipleRefsAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasResumptionStubFixup(reader, "CaptureObjectAcrossAwait", out diag), diag); } } @@ -258,8 +258,7 @@ public void RuntimeAsyncDevirtualize() AssemblyName = nameof(RuntimeAsyncDevirtualize), SourceResourceNames = [ - "RuntimeAsync/AwaitsThroughInterface.cs", - "RuntimeAsync/Dependencies/AsyncInterfaceAndImpls.cs", + "RuntimeAsync/AsyncDevirtualize.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -293,7 +292,7 @@ public void RuntimeAsyncNoYield() AssemblyName = nameof(RuntimeAsyncNoYield), SourceResourceNames = [ - "RuntimeAsync/AsyncWithoutYield.cs", + "RuntimeAsync/AsyncNoYield.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -323,12 +322,12 @@ static void Validate(ReadyToRunReader reader) [Fact] public void RuntimeAsyncCrossModule() { - var inlinableAsyncMethods = new CompiledAssembly + var asyncDepLib = new CompiledAssembly { - AssemblyName = "InlineCandidateMatrix", + AssemblyName = "AsyncDepLib", SourceResourceNames = [ - "RuntimeAsync/Dependencies/InlineCandidateMatrix.cs", + "RuntimeAsync/Dependencies/AsyncDepLib.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -338,11 +337,11 @@ public void RuntimeAsyncCrossModule() AssemblyName = nameof(RuntimeAsyncCrossModule), SourceResourceNames = [ - "RuntimeAsync/AwaitsInlinableAsync.cs", + "RuntimeAsync/AsyncCrossModule.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [inlinableAsyncMethods] + References = [asyncDepLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -351,7 +350,7 @@ public void RuntimeAsyncCrossModule() new(nameof(RuntimeAsyncCrossModule), [ new CrossgenAssembly(runtimeAsyncCrossModule), - new CrossgenAssembly(inlinableAsyncMethods) + new CrossgenAssembly(asyncDepLib) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -365,8 +364,8 @@ public void RuntimeAsyncCrossModule() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlineCandidateMatrix", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallGetValueAsync", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLib", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleAsync", out diag), diag); } } @@ -382,16 +381,16 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeCrossModuleInlining() { - var syncInlinableMethods = new CompiledAssembly + var inlineableLib = new CompiledAssembly { - AssemblyName = "SyncInlinableMethods", - SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncInlinableMethods.cs"], + AssemblyName = "InlineableLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], }; var compositeMain = new CompiledAssembly { AssemblyName = "CompositeCrossModuleInlining", SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], - References = [syncInlinableMethods] + References = [inlineableLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -399,7 +398,7 @@ public void CompositeCrossModuleInlining() [ new(nameof(CompositeCrossModuleInlining), [ - new CrossgenAssembly(syncInlinableMethods), + new CrossgenAssembly(inlineableLib), new CrossgenAssembly(compositeMain), ]) { @@ -411,7 +410,7 @@ public void CompositeCrossModuleInlining() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "SyncInlinableMethods", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLib", out diag), diag); Assert.True(R2RAssert.HasInlinedMethod(reader, "TestGetValue", "GetValue", out diag), diag); } } @@ -432,16 +431,16 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeDoesNotProduceCrossModuleInliningInfo() { - var syncInlinableMethods = new CompiledAssembly + var inlineableLib = new CompiledAssembly { - AssemblyName = "SyncInlinableMethods", - SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncInlinableMethods.cs"], + AssemblyName = "InlineableLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], }; var compositeMain = new CompiledAssembly { AssemblyName = nameof(CompositeDoesNotProduceCrossModuleInliningInfo), SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], - References = [syncInlinableMethods] + References = [inlineableLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -449,7 +448,7 @@ public void CompositeDoesNotProduceCrossModuleInliningInfo() [ new(nameof(CompositeDoesNotProduceCrossModuleInliningInfo), [ - new CrossgenAssembly(syncInlinableMethods), + new CrossgenAssembly(inlineableLib), new CrossgenAssembly(compositeMain), ]) { @@ -477,18 +476,18 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeAsync() { - var inlinableAsyncMethods = new CompiledAssembly + var asyncCompositeLib = new CompiledAssembly { - AssemblyName = "InlineCandidateMatrix", - SourceResourceNames = ["RuntimeAsync/Dependencies/InlineCandidateMatrix.cs"], + AssemblyName = "AsyncCompositeLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], Features = { RuntimeAsyncFeature }, }; - var awaitsInlinableAsync = new CompiledAssembly + var compositeAsyncMain = new CompiledAssembly { - AssemblyName = "AwaitsInlinableAsync", - SourceResourceNames = ["RuntimeAsync/AwaitsInlinableAsync.cs"], + AssemblyName = "CompositeAsyncMain", + SourceResourceNames = ["CrossModuleInlining/CompositeAsync.cs"], Features = { RuntimeAsyncFeature }, - References = [inlinableAsyncMethods] + References = [asyncCompositeLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -496,8 +495,8 @@ public void CompositeAsync() [ new(nameof(CompositeAsync), [ - new CrossgenAssembly(inlinableAsyncMethods), - new CrossgenAssembly(awaitsInlinableAsync), + new CrossgenAssembly(asyncCompositeLib), + new CrossgenAssembly(compositeAsyncMain), ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], @@ -508,15 +507,15 @@ public void CompositeAsync() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlineCandidateMatrix", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallGetValueAsync", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "ReturnTaskPrimitiveWithAwait", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncCompositeLib", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCompositeAsync", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "GetValueAsync", out diag), diag); } } /// /// The full intersection: composite + runtime-async + cross-module inlining. - /// Async methods from InlineCandidateMatrix are inlined into AwaitsInlinableAsync + /// Async methods from AsyncCompositeLib are inlined into CompositeAsyncMain /// within a composite image, exercising MutableModule token encoding for /// cross-module async continuation layouts. /// @@ -536,14 +535,14 @@ public void CompositeAsyncInliningMatrix() { var asyncInlineCandidatesLib = new CompiledAssembly { - AssemblyName = "InlineCandidateMatrix", - SourceResourceNames = ["RuntimeAsync/Dependencies/InlineCandidateMatrix.cs"], + AssemblyName = "AsyncInlineCandidatesLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs"], Features = { RuntimeAsyncFeature }, }; var asyncInlineCallers = new CompiledAssembly { - AssemblyName = "AwaitsInlineCandidateMatrix", - SourceResourceNames = ["RuntimeAsync/AwaitsInlineCandidateMatrix.cs"], + AssemblyName = "AsyncInlineCallers", + SourceResourceNames = ["CrossModuleInlining/AsyncInlineCallers.cs"], Features = { RuntimeAsyncFeature }, References = [asyncInlineCandidatesLib] }; @@ -565,7 +564,7 @@ public void CompositeAsyncInliningMatrix() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlineCandidateMatrix", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncInlineCandidatesLib", out diag), diag); // Awaitless async candidates: should be inlined into their callers. Assert.True(R2RAssert.HasInlinedMethod(reader, "CallReturnTaskNoAwait", "ReturnTaskNoAwait", out diag), diag); @@ -588,26 +587,26 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeAsyncContinuationAndResume() { - var locals = new CompiledAssembly + var asyncCompositeContLib = new CompiledAssembly { - AssemblyName = "LocalsCapturedAcrossAwait", + AssemblyName = "AsyncCompositeContLib", SourceResourceNames = [ - "RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs", + "RuntimeAsync/Dependencies/AsyncCompositeContLib.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, }; - var awaitsLocals = new CompiledAssembly + var compositeAsyncContMain = new CompiledAssembly { - AssemblyName = "AwaitsLocalsCapturedAcrossAwait", + AssemblyName = "CompositeAsyncContinuationMain", SourceResourceNames = [ - "RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs", + "RuntimeAsync/CompositeAsyncContinuationMain.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [locals] + References = [asyncCompositeContLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -615,8 +614,8 @@ public void CompositeAsyncContinuationAndResume() [ new(nameof(CompositeAsyncContinuationAndResume), [ - new CrossgenAssembly(locals), - new CrossgenAssembly(awaitsLocals), + new CrossgenAssembly(asyncCompositeContLib), + new CrossgenAssembly(compositeAsyncContMain), ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], @@ -628,19 +627,19 @@ static void Validate(ReadyToRunReader reader) { string diag; // Library methods produce async variants and resume stubs - Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureArrayAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureRefAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureArrayAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefComposite", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureArrayComposite", out diag), diag); + Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureRefComposite", out diag), diag); + Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureArrayComposite", out diag), diag); // Main assembly methods produce async variants and resume stubs - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureRefAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureRefComposite", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "LocalCaptureAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasResumptionStub(reader, "CallCaptureRefAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasResumptionStub(reader, "CallCaptureRefComposite", out diag), diag); Assert.True(R2RAssert.HasResumptionStub(reader, "LocalCaptureAcrossAwait", out diag), diag); // ContinuationLayout fixups are present for methods with GC refs across awaits - Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureRefAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureRefComposite", out diag), diag); Assert.True(R2RAssert.HasContinuationLayout(reader, "LocalCaptureAcrossAwait", out diag), diag); } } @@ -663,20 +662,20 @@ public void CompositeAsyncGenericTypes() { var asyncGenericTypeLib = new CompiledAssembly { - AssemblyName = "AwaitsAsyncMethodsOnGenericType.GenericContainer", + AssemblyName = "AsyncGenericTypeLib", SourceResourceNames = [ - "RuntimeAsync/Dependencies/GenericContainer.cs", + "RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, }; var compositeAsyncGenericTypesMain = new CompiledAssembly { - AssemblyName = "AwaitsAsyncMethodsOnGenericType", + AssemblyName = "CompositeAsyncGenericTypesMain", SourceResourceNames = [ - "RuntimeAsync/AwaitsAsyncMethodsOnGenericType.cs", + "RuntimeAsync/CompositeAsyncGenericTypesMain.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, @@ -726,26 +725,26 @@ static void Validate(ReadyToRunReader reader) [Fact] public void AsyncCrossModuleContinuation() { - var locals = new CompiledAssembly + var asyncDepLibCont = new CompiledAssembly { - AssemblyName = "LocalsCapturedAcrossAwait", + AssemblyName = "AsyncDepLibContinuation", SourceResourceNames = [ - "RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs", + "RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, }; - var awaitsLocals = new CompiledAssembly + var asyncCrossModuleCont = new CompiledAssembly { AssemblyName = nameof(AsyncCrossModuleContinuation), SourceResourceNames = [ - "RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs", + "RuntimeAsync/AsyncCrossModuleContinuation.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [locals] + References = [asyncDepLibCont] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -753,8 +752,8 @@ public void AsyncCrossModuleContinuation() [ new(nameof(AsyncCrossModuleContinuation), [ - new CrossgenAssembly(awaitsLocals), - new CrossgenAssembly(locals) + new CrossgenAssembly(asyncCrossModuleCont), + new CrossgenAssembly(asyncDepLibCont) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -768,9 +767,9 @@ public void AsyncCrossModuleContinuation() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "LocalsCapturedAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureRefAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureArrayAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureArray", out diag), diag); } } @@ -783,13 +782,13 @@ public void MultiStepCompositeAndNonComposite() { var libA = new CompiledAssembly { - AssemblyName = "MultiStepLeaf", - SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepLeaf.cs"], + AssemblyName = "MultiStepLibA", + SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepLibA.cs"], }; var libB = new CompiledAssembly { - AssemblyName = "MultiStepMid", - SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepMid.cs"], + AssemblyName = "MultiStepLibB", + SourceResourceNames = ["CrossModuleInlining/Dependencies/MultiStepLibB.cs"], References = [libA] }; var consumer = new CompiledAssembly @@ -812,7 +811,7 @@ public void MultiStepCompositeAndNonComposite() Validate = reader => { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "MultiStepLeaf", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "MultiStepLibA", out diag), diag); }, }, new("NonCompositeStep", @@ -828,7 +827,7 @@ public void MultiStepCompositeAndNonComposite() Validate = reader => { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "MultiStepLeaf", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "MultiStepLibA", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "GetValueFromLibA", "GetValue", out diag), diag); }, }, @@ -846,22 +845,22 @@ public void MultiStepCompositeAndNonComposite() [Fact] public void CompositeAsyncDevirtualize() { - var interfaceAndImpls = new CompiledAssembly + var asyncInterfaceLib = new CompiledAssembly { - AssemblyName = "InterfaceAndImpls", - SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncInterfaceAndImpls.cs"], + AssemblyName = "AsyncInterfaceLib", + SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncInterfaceLib.cs"], Features = { RuntimeAsyncFeature }, }; - var awaitsThroughInterface = new CompiledAssembly + var compositeDevirtMain = new CompiledAssembly { - AssemblyName = "AwaitsThroughInterface", + AssemblyName = "CompositeAsyncDevirtMain", SourceResourceNames = [ - "RuntimeAsync/AwaitsThroughInterface.cs", + "RuntimeAsync/CompositeAsyncDevirtMain.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [interfaceAndImpls] + References = [asyncInterfaceLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -869,8 +868,8 @@ public void CompositeAsyncDevirtualize() [ new(nameof(CompositeAsyncDevirtualize), [ - new CrossgenAssembly(interfaceAndImpls), - new CrossgenAssembly(awaitsThroughInterface), + new CrossgenAssembly(asyncInterfaceLib), + new CrossgenAssembly(compositeDevirtMain), ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], @@ -881,7 +880,7 @@ public void CompositeAsyncDevirtualize() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InterfaceAndImpls", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncInterfaceLib", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "CallOnSealed", out diag), diag); } } @@ -893,22 +892,22 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeTransitive() { - var syncLeafMethods = new CompiledAssembly + var externalLib = new CompiledAssembly { - AssemblyName = "SyncLeafMethods", - SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncLeafMethods.cs"], + AssemblyName = "ExternalLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/ExternalLib.cs"], }; - var inlinableLeafCallers = new CompiledAssembly + var inlineableLibTransitive = new CompiledAssembly { - AssemblyName = "InlinableLeafCallers", - SourceResourceNames = ["CrossModuleInlining/Dependencies/InlinableLeafCallers.cs"], - References = [syncLeafMethods] + AssemblyName = "InlineableLibTransitive", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLibTransitive.cs"], + References = [externalLib] }; var compositeTransitiveMain = new CompiledAssembly { AssemblyName = "CompositeTransitive", SourceResourceNames = ["CrossModuleInlining/TransitiveReferences.cs"], - References = [inlinableLeafCallers, syncLeafMethods] + References = [inlineableLibTransitive, externalLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -916,8 +915,8 @@ public void CompositeTransitive() [ new(nameof(CompositeTransitive), [ - new CrossgenAssembly(syncLeafMethods), - new CrossgenAssembly(inlinableLeafCallers), + new CrossgenAssembly(externalLib), + new CrossgenAssembly(inlineableLibTransitive), new CrossgenAssembly(compositeTransitiveMain), ]) { @@ -929,8 +928,8 @@ public void CompositeTransitive() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlinableLeafCallers", out diag), diag); - Assert.True(R2RAssert.HasManifestRef(reader, "SyncLeafMethods", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLibTransitive", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "ExternalLib", out diag), diag); } } @@ -941,32 +940,32 @@ static void Validate(ReadyToRunReader reader) [Fact] public void AsyncCrossModuleTransitive() { - var syncLeafMethods = new CompiledAssembly + var asyncExternalLib = new CompiledAssembly { - AssemblyName = "SyncLeafMethods", - SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncLeafMethods.cs"], + AssemblyName = "AsyncExternalLib", + SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncExternalLib.cs"], }; - var inlinableAsyncLeafCallers = new CompiledAssembly + var asyncTransitiveLib = new CompiledAssembly { - AssemblyName = "InlinableAsyncLeafCallers", + AssemblyName = "AsyncTransitiveLib", SourceResourceNames = [ - "RuntimeAsync/Dependencies/InlinableAsyncLeafCallers.cs", + "RuntimeAsync/Dependencies/AsyncTransitiveLib.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [syncLeafMethods] + References = [asyncExternalLib] }; - var awaitsTransitiveAsync = new CompiledAssembly + var asyncTransitiveMain = new CompiledAssembly { AssemblyName = nameof(AsyncCrossModuleTransitive), SourceResourceNames = [ - "RuntimeAsync/AwaitsTransitiveAsync.cs", + "RuntimeAsync/AsyncTransitiveMain.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [inlinableAsyncLeafCallers, syncLeafMethods] + References = [asyncTransitiveLib, asyncExternalLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -974,9 +973,9 @@ public void AsyncCrossModuleTransitive() [ new(nameof(AsyncCrossModuleTransitive), [ - new CrossgenAssembly(awaitsTransitiveAsync), - new CrossgenAssembly(syncLeafMethods) { Kind = Crossgen2InputKind.Reference }, - new CrossgenAssembly(inlinableAsyncLeafCallers) + new CrossgenAssembly(asyncTransitiveMain), + new CrossgenAssembly(asyncExternalLib) { Kind = Crossgen2InputKind.Reference }, + new CrossgenAssembly(asyncTransitiveLib) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -990,7 +989,7 @@ public void AsyncCrossModuleTransitive() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlinableAsyncLeafCallers", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync", out diag), diag); } } @@ -1002,32 +1001,32 @@ static void Validate(ReadyToRunReader reader) [Fact] public void CompositeAsyncTransitive() { - var syncLeafMethods = new CompiledAssembly + var asyncExternalLib = new CompiledAssembly { - AssemblyName = "SyncLeafMethods", - SourceResourceNames = ["CrossModuleInlining/Dependencies/SyncLeafMethods.cs"], + AssemblyName = "AsyncExternalLib", + SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncExternalLib.cs"], }; - var inlinableAsyncLeafCallers = new CompiledAssembly + var asyncTransitiveLib = new CompiledAssembly { - AssemblyName = "InlinableAsyncLeafCallers", + AssemblyName = "AsyncTransitiveLib", SourceResourceNames = [ - "RuntimeAsync/Dependencies/InlinableAsyncLeafCallers.cs", + "RuntimeAsync/Dependencies/AsyncTransitiveLib.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [syncLeafMethods] + References = [asyncExternalLib] }; var compositeAsyncTransitiveMain = new CompiledAssembly { AssemblyName = "CompositeAsyncTransitive", SourceResourceNames = [ - "RuntimeAsync/AwaitsTransitiveAsync.cs", + "RuntimeAsync/AsyncTransitiveMain.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [inlinableAsyncLeafCallers, syncLeafMethods] + References = [asyncTransitiveLib, asyncExternalLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -1035,8 +1034,8 @@ public void CompositeAsyncTransitive() [ new(nameof(CompositeAsyncTransitive), [ - new CrossgenAssembly(syncLeafMethods), - new CrossgenAssembly(inlinableAsyncLeafCallers), + new CrossgenAssembly(asyncExternalLib), + new CrossgenAssembly(asyncTransitiveLib), new CrossgenAssembly(compositeAsyncTransitiveMain), ]) { @@ -1048,7 +1047,7 @@ public void CompositeAsyncTransitive() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "InlinableAsyncLeafCallers", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncTransitiveLib", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "CallTransitiveValueAsync", out diag), diag); } } @@ -1061,16 +1060,16 @@ static void Validate(ReadyToRunReader reader) [Fact] public void MultiStepCompositeAndNonCompositeAsync() { - var locals = new CompiledAssembly + var asyncDepLib = new CompiledAssembly { - AssemblyName = "LocalsCapturedAcrossAwait", - SourceResourceNames = ["RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs"], + AssemblyName = "AsyncDepLibContinuation", + SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs"], Features = { RuntimeAsyncFeature }, }; - var inlinableAsyncMethods = new CompiledAssembly + var asyncCompositeLib = new CompiledAssembly { - AssemblyName = "InlineCandidateMatrix", - SourceResourceNames = ["RuntimeAsync/Dependencies/InlineCandidateMatrix.cs"], + AssemblyName = "AsyncCompositeLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCompositeLib.cs"], Features = { RuntimeAsyncFeature }, }; var asyncConsumer = new CompiledAssembly @@ -1078,11 +1077,11 @@ public void MultiStepCompositeAndNonCompositeAsync() AssemblyName = "MultiStepAsyncConsumer", SourceResourceNames = [ - "RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs", + "RuntimeAsync/AsyncCrossModuleContinuation.cs", "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", ], Features = { RuntimeAsyncFeature }, - References = [locals] + References = [asyncDepLib] }; new R2RTestRunner(_output).Run(new R2RTestCase( @@ -1090,22 +1089,22 @@ public void MultiStepCompositeAndNonCompositeAsync() [ new("CompositeAsyncStep", [ - new CrossgenAssembly(locals), - new CrossgenAssembly(inlinableAsyncMethods), + new CrossgenAssembly(asyncDepLib), + new CrossgenAssembly(asyncCompositeLib), ]) { Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], Validate = reader => { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "LocalsCapturedAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation", out diag), diag); Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefAcrossAwait", out diag), diag); }, }, new("NonCompositeAsyncStep", [ new CrossgenAssembly(asyncConsumer), - new CrossgenAssembly(locals) + new CrossgenAssembly(asyncDepLib) { Kind = Crossgen2InputKind.Reference, Options = [Crossgen2AssemblyOption.CrossModuleOptimization], @@ -1115,8 +1114,8 @@ public void MultiStepCompositeAndNonCompositeAsync() Validate = reader => { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "LocalsCapturedAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureRefAcrossAwait", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDepLibContinuation", out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCrossModuleCaptureRef", out diag), diag); }, }, ])); @@ -1134,8 +1133,8 @@ public void CrossModuleGenericMultiInliner() { var crossModuleGenericLib = new CompiledAssembly { - AssemblyName = "MultiInlinerConsumer.GenericWrappers", - SourceResourceNames = ["CrossModuleInlining/Dependencies/GenericWrappers.cs"], + AssemblyName = "CrossModuleGenericLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs"], }; var consumer = new CompiledAssembly { @@ -1164,7 +1163,7 @@ public void CrossModuleGenericMultiInliner() static void Validate(ReadyToRunReader reader) { string diag; - Assert.True(R2RAssert.HasManifestRef(reader, "MultiInlinerConsumer.GenericWrappers", out diag), diag); + Assert.True(R2RAssert.HasManifestRef(reader, "CrossModuleGenericLib", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInliningInfo(reader, out diag), diag); // Verify that GetValue has cross-module inliners from both GenericWrapperA and GenericWrapperB. diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs new file mode 100644 index 00000000000000..cc1853b48a5bb7 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs @@ -0,0 +1,28 @@ +// Test: Cross-module async method inlining +// Validates that cross-module async compilation produces manifest refs +// and [ASYNC] variants for methods calling into a dependency library. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncCrossModule +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCrossModuleAsync() + { + return await AsyncDepLib.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCrossModuleStringAsync() + { + return await AsyncDepLib.GetStringAsync(); + } + + // Call a non-async sync method from async lib + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CallCrossModuleSync() + { + return AsyncDepLib.GetValueSync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs new file mode 100644 index 00000000000000..b4e2f87ecda45d --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs @@ -0,0 +1,21 @@ +// Test: Non-composite runtime-async cross-module inlining with continuation layouts. +// The dependency methods capture GC refs across await points. +// Validates manifest refs and [ASYNC] variants for cross-module async calls. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncCrossModuleContinuation +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCrossModuleCaptureRef() + { + return await AsyncDepLibContinuation.CaptureRefAcrossAwait(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCrossModuleCaptureArray() + { + return await AsyncDepLibContinuation.CaptureArrayAcrossAwait(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs new file mode 100644 index 00000000000000..6170e4ac67360a --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs @@ -0,0 +1,56 @@ +// Test: Async virtual method devirtualization in R2R +// Validates that async methods on sealed/interface types +// produce [ASYNC] variant entries in the R2R image. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public interface IAsyncService +{ + Task GetValueAsync(); +} + +public class OpenImpl : IAsyncService +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public virtual async Task GetValueAsync() + { + await Task.Yield(); + return 10; + } +} + +public sealed class SealedImpl : IAsyncService +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task GetValueAsync() + { + await Task.Yield(); + return 20; + } +} + +public static class AsyncDevirtualize +{ + // Sealed type known at compile time — should devirtualize + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallOnSealed(SealedImpl obj) + { + return await obj.GetValueAsync(); + } + + // newobj gives exact type info — should devirtualize through resolveVirtualMethod + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallOnNewOpen() + { + IAsyncService svc = new OpenImpl(); + return await svc.GetValueAsync(); + } + + // Generic constrained dispatch + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallGenericConstrained(T obj) where T : IAsyncService + { + return await obj.GetValueAsync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithoutYield.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs similarity index 94% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithoutYield.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs index 8d2c1140a8e572..28ddb07949521c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithoutYield.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class AsyncWithoutYield +public static class AsyncNoYield { [MethodImpl(MethodImplOptions.NoInlining)] public static async Task AsyncButNoAwait() diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs new file mode 100644 index 00000000000000..e92cd6f1ae2de4 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs @@ -0,0 +1,21 @@ +// Test: Non-composite runtime-async transitive cross-module inlining. +// Chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. +// Validates transitive manifest refs and async cross-module inlining. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncTransitiveMain +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallTransitiveValueAsync() + { + return await AsyncTransitiveLib.GetExternalValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallTransitiveLabelAsync() + { + return await AsyncTransitiveLib.GetExternalLabelAsync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs new file mode 100644 index 00000000000000..29a8fc879ec463 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs @@ -0,0 +1,26 @@ +// Test: Async method that captures GC refs across await +// This forces the compiler to emit a ContinuationLayout fixup. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncWithContinuation +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CaptureObjectAcrossAwait() + { + object o = new object(); + string s = "hello"; + await Task.Yield(); + return s + o.GetHashCode(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CaptureMultipleRefsAcrossAwait() + { + int[] arr = new int[] { 1, 2, 3 }; + string text = "world"; + await Task.Yield(); + return arr[0] + text.Length; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs deleted file mode 100644 index 8d8b24e45bcfc3..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsInlinableAsync.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Awaits a small subset of InlineCandidateMatrix methods (one Task, one -// Task, one sync) so that cross-module and composite tests can observe -// async-variant emission and cross-module inlining of inlinable async methods. -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AwaitsInlinableAsync -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallGetValueAsync() - { - return await InlineCandidateMatrix.ReturnTaskPrimitiveWithAwait(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallGetStringAsync() - { - return await InlineCandidateMatrix.ReturnTaskClassWithAwait(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static int CallGetValueSync() - { - return InlineCandidateMatrix.GetValueSync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs deleted file mode 100644 index 53e5de86c7c8e3..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsLocalsCapturedAcrossAwait.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Awaits the methods in LocalsCapturedAcrossAwait, plus a self-contained -// async method that captures locals across an await in this assembly itself. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AwaitsLocalsCapturedAcrossAwait -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCaptureRefAcrossAwait() - { - return await LocalsCapturedAcrossAwait.CaptureRefAcrossAwait(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCaptureArrayAcrossAwait() - { - return await LocalsCapturedAcrossAwait.CaptureArrayAcrossAwait(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task LocalCaptureAcrossAwait() - { - object o = new object(); - string s = "local"; - await Task.Yield(); - return s.Length + o.GetHashCode(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsThroughInterface.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsThroughInterface.cs deleted file mode 100644 index aacb26c429503d..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsThroughInterface.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Calls an async interface method via three dispatch shapes: sealed type, -// newobj-then-interface, and generic-constrained. Consumers of -// InterfaceAndImpls to exercise devirtualization of async dispatch. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AwaitsThroughInterface -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallOnSealed(SealedImpl obj) - { - return await obj.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallOnNewOpen() - { - IAsyncService svc = new OpenImpl(); - return await svc.GetValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallGenericConstrained(T obj) where T : IAsyncService - { - return await obj.GetValueAsync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsTransitiveAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsTransitiveAsync.cs deleted file mode 100644 index 2769922e95a273..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsTransitiveAsync.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Awaits through InlinableAsyncLeafCallers so the transitive reference to -// SyncLeafMethods is exercised under runtime-async. -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AwaitsTransitiveAsync -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallTransitiveValueAsync() - { - return await InlinableAsyncLeafCallers.GetExternalValueAsync(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallTransitiveLabelAsync() - { - return await InlinableAsyncLeafCallers.GetExternalLabelAsync(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs new file mode 100644 index 00000000000000..2419d72ac82af4 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs @@ -0,0 +1,30 @@ +// Test: Composite mode async with continuation layouts and resumption stubs. +// Calls async methods from AsyncCompositeContLib that capture GC refs across +// await points, exercising composite-mode ContinuationLayout and RESUME stub emission. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class CompositeAsyncContinuationMain +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCaptureRefComposite() + { + return await AsyncCompositeContLib.CaptureRefComposite(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallCaptureArrayComposite() + { + return await AsyncCompositeContLib.CaptureArrayComposite(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task LocalCaptureAcrossAwait() + { + object o = new object(); + string s = "local"; + await Task.Yield(); + return s.Length + o.GetHashCode(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs new file mode 100644 index 00000000000000..62a06bc816f0c2 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs @@ -0,0 +1,22 @@ +// Test: Composite mode async devirtualization across module boundaries. +// Interface defined in AsyncInterfaceLib, call sites here. +// In composite mode, crossgen2 should devirtualize sealed type dispatch. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class CompositeAsyncDevirtMain +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallOnSealed(SealedAsyncService svc) + { + return await svc.GetValueAsync(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CallOnNewOpen() + { + IAsyncCompositeService svc = new OpenAsyncService(); + return await svc.GetValueAsync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsAsyncMethodsOnGenericType.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs similarity index 96% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsAsyncMethodsOnGenericType.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs index 5e6928aea5699b..8dbd03c5de1fd1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AwaitsAsyncMethodsOnGenericType.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs @@ -11,7 +11,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class AwaitsAsyncMethodsOnGenericType +public static class CompositeAsyncGenericTypesMain { [MethodImpl(MethodImplOptions.NoInlining)] public static async Task CallGenericContainerInt() diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs new file mode 100644 index 00000000000000..85ff77178e7502 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs @@ -0,0 +1,28 @@ +// Dependency library for composite async continuation tests. +// Contains runtime-async methods that capture GC refs across await points. +// Used in composite mode to exercise MutableModule token encoding for +// cross-module async continuation layouts and resumption stubs. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncCompositeContLib +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CaptureRefComposite() + { + object o = new object(); + string s = "composite_ref"; + await Task.Yield(); + return s + o.GetHashCode(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task CaptureArrayComposite() + { + int[] arr = new int[] { 5, 10, 15 }; + string label = "total"; + await Task.Yield(); + return arr[0] + arr[1] + label.Length; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs new file mode 100644 index 00000000000000..bb10453f70b05e --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs @@ -0,0 +1,28 @@ +// Dependency library for async cross-module tests. +// Contains runtime-async methods that should be inlineable. +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncDepLib +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetValueAsync() + { + await Task.Yield(); + return 42; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetStringAsync() + { + await Task.Yield(); + return "async_hello"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetValueSync() + { + return 99; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs similarity index 54% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs index a7c3e0cbac13e0..b4124b80008fe1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/LocalsCapturedAcrossAwait.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs @@ -1,22 +1,22 @@ -// Runtime-async methods that capture GC refs across an await point. -// These force ContinuationLayout fixup emission when they are called -// (and optionally cross-module inlined) by another assembly. +// Dependency library for async cross-module continuation tests. +// Contains runtime-async methods that capture GC refs across await points, +// forcing ContinuationLayout fixup emission when cross-module inlined. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class LocalsCapturedAcrossAwait +public static class AsyncDepLibContinuation { - [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static async Task CaptureRefAcrossAwait() { object o = new object(); - string s = "captured"; + string s = "cross_module"; await Task.Yield(); return s + o.GetHashCode(); } - [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static async Task CaptureArrayAcrossAwait() { int[] arr = new int[] { 10, 20, 30 }; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs new file mode 100644 index 00000000000000..f45db23f004570 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs @@ -0,0 +1,13 @@ +// External types for async transitive cross-module tests. +// Similar to ExternalLib but with async-friendly types. +using System; + +public static class AsyncExternalLib +{ + public static int ExternalValue => 77; + + public class AsyncExternalType + { + public string Label { get; set; } = "external"; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/GenericContainer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/GenericContainer.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceAndImpls.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs similarity index 56% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceAndImpls.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs index 327130ca8d1605..528162b5d10c10 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceAndImpls.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs @@ -1,26 +1,25 @@ -// Interface + two implementations (sealed and non-sealed/open) of an -// async-returning method. Used by consumers to exercise devirtualization -// of async interface dispatch. +// Dependency library: defines an async interface and sealed implementation +// for cross-module async devirtualization tests in composite mode. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -public interface IAsyncService +public interface IAsyncCompositeService { Task GetValueAsync(); } -public sealed class SealedImpl : IAsyncService +public sealed class SealedAsyncService : IAsyncCompositeService { [MethodImpl(MethodImplOptions.NoInlining)] public async Task GetValueAsync() { await Task.Yield(); - return 20; + return 42; } } -public class OpenImpl : IAsyncService +public class OpenAsyncService : IAsyncCompositeService { [MethodImpl(MethodImplOptions.NoInlining)] public virtual async Task GetValueAsync() diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlinableAsyncLeafCallers.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs similarity index 55% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlinableAsyncLeafCallers.cs rename to src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs index 04ac8012b0abbd..717040a417c362 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/InlinableAsyncLeafCallers.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs @@ -1,22 +1,22 @@ -// Inlinable async helpers that forward through to SyncLeafMethods. -// Used to exercise transitive cross-module inlining for runtime-async. +// Middle library in async transitive chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. +// Contains runtime-async methods that reference types from AsyncExternalLib. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -public static class InlinableAsyncLeafCallers +public static class AsyncTransitiveLib { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static async Task GetExternalValueAsync() { await Task.Yield(); - return SyncLeafMethods.ExternalValue; + return AsyncExternalLib.ExternalValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static async Task GetExternalLabelAsync() { - var ext = new SyncLeafMethods.ExternalType(); + var ext = new AsyncExternalLib.AsyncExternalType(); await Task.Yield(); return ext.Label; } From f64133911ed3841d9b0dfbf1294858cee92a9873 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:21:35 -0700 Subject: [PATCH 56/74] Replace ad-hoc header comments in R2R test fixtures with MIT license header Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CrossModuleInlining/AsyncInlineCallers.cs | 6 +++--- .../TestCases/CrossModuleInlining/AsyncMethods.cs | 6 +++--- .../TestCases/CrossModuleInlining/BasicInlining.cs | 6 +++--- .../TestCases/CrossModuleInlining/CompositeAsync.cs | 5 +++-- .../TestCases/CrossModuleInlining/CompositeBasic.cs | 6 +++--- .../Dependencies/AsyncCompositeLib.cs | 5 +++-- .../Dependencies/AsyncInlineCandidatesLib.cs | 9 +++------ .../Dependencies/AsyncInlineableLib.cs | 3 +++ .../Dependencies/CompositeLib.cs | 3 +++ .../Dependencies/CrossModuleGenericLib.cs | 3 +++ .../CrossModuleInlining/Dependencies/ExternalLib.cs | 3 +++ .../Dependencies/InlineableLib.cs | 3 +++ .../Dependencies/InlineableLibTransitive.cs | 3 +++ .../Dependencies/MultiStepLibA.cs | 5 +++-- .../Dependencies/MultiStepLibB.cs | 5 +++-- .../CrossModuleInlining/MultiInlinerConsumer.cs | 13 ++----------- .../CrossModuleInlining/MultiStepConsumer.cs | 6 +++--- .../CrossModuleInlining/TransitiveReferences.cs | 6 +++--- .../TestCases/RuntimeAsync/AsyncCrossModule.cs | 6 +++--- .../RuntimeAsync/AsyncCrossModuleContinuation.cs | 6 +++--- .../TestCases/RuntimeAsync/AsyncDevirtualize.cs | 6 +++--- .../TestCases/RuntimeAsync/AsyncNoYield.cs | 6 +++--- .../TestCases/RuntimeAsync/AsyncTransitiveMain.cs | 6 +++--- .../TestCases/RuntimeAsync/AsyncWithContinuation.cs | 5 +++-- .../TestCases/RuntimeAsync/BasicAsyncEmission.cs | 6 +++--- .../RuntimeAsync/CompositeAsyncContinuationMain.cs | 6 +++--- .../RuntimeAsync/CompositeAsyncDevirtMain.cs | 6 +++--- .../RuntimeAsync/CompositeAsyncGenericTypesMain.cs | 12 +++--------- .../Dependencies/AsyncCompositeContLib.cs | 7 +++---- .../RuntimeAsync/Dependencies/AsyncDepLib.cs | 5 +++-- .../Dependencies/AsyncDepLibContinuation.cs | 6 +++--- .../RuntimeAsync/Dependencies/AsyncExternalLib.cs | 5 +++-- .../Dependencies/AsyncGenericTypeLib.cs | 10 +++------- .../RuntimeAsync/Dependencies/AsyncInterfaceLib.cs | 5 +++-- .../RuntimeAsync/Dependencies/AsyncTransitiveLib.cs | 5 +++-- 35 files changed, 104 insertions(+), 100 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs index da1e0dff49d4f8..50bde024a8ca56 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncInlineCallers.cs @@ -1,6 +1,6 @@ -// Six call sites that each invoke one of the AsyncInlineCandidatesLib methods. -// Each caller is marked NoInlining so it stays put as the inliner; the test -// then asserts which candidates get inlined into their respective callers. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs index fdf1b159130b95..ecc124759fc32e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs @@ -1,6 +1,6 @@ -// Test: Cross-module inlining of async methods -// Validates that async methods from AsyncInlineableLib are cross-module -// inlined into this assembly with CHECK_IL_BODY fixups. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs index ba301272868c25..5781e4d8d0de2b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs @@ -1,6 +1,6 @@ -// Test: Basic cross-module inlining -// Validates that crossgen2 with --opt-cross-module produces CHECK_IL_BODY fixups -// for methods inlined from InlineableLib into this main assembly. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs index 7e19cc7eea200c..08409268fb4d9c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs @@ -1,5 +1,6 @@ -// Test: Composite mode with runtime-async methods across assemblies. -// Validates that async methods produce [ASYNC] variants in composite output. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs index 0ba24d5563555c..d71478cea4ff4b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs @@ -1,6 +1,6 @@ -// Test: Composite mode basic compilation -// Validates that composite mode R2R compilation with multiple assemblies -// produces correct manifest references and component assembly entries. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs index bece15b74b11b6..9d8a6782d4e3f8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs @@ -1,5 +1,6 @@ -// Dependency library for composite async tests. -// Contains runtime-async methods called from another assembly in composite mode. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs index d4d40a11218fe4..17d31e2f8f16a8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineCandidatesLib.cs @@ -1,9 +1,6 @@ -// Dependency library exposing six runtime-async inlining candidates that cross -// the {Task, Task, Task} x {with-await, without-await} matrix. -// The JIT cannot inline an async method that performs an actual await -// (Compiler::impSetupAsyncCall reports CALLEE_AWAIT FATAL in importercalls.cpp; -// AsyncSuspend reports CALLEE_ASYNC_SUSPEND FATAL). Methods without an await -// are eligible for inlining like any other small method. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs index 153804e6279fc1..1cfd143829072c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs index 2dc5db2de38bcc..c0b3a828697b4f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; public static class CompositeLib diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs index b17643fb9eb99b..c354a0fc7c587d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.CompilerServices; /// diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs index d56f2880564a13..9fc951a482e107 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; public static class ExternalLib diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs index a799cfed7282e8..a6834e200ffec4 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs index 15fd29dda19d4b..5412a828c8582f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs index c47dd6ac274f28..d9785e920d9f1e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs @@ -1,5 +1,6 @@ -// Dependency library for multi-step compilation tests. -// Contains sync inlineable methods used in both composite and non-composite steps. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs index cf8acd54d8bd9b..7e839815af04b1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs @@ -1,5 +1,6 @@ -// Second library for multi-step composite compilation. -// Compiled together with MultiStepLibA as a composite in step 1. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs index 56e9c37d33b7f5..3956103fe15587 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs @@ -1,14 +1,5 @@ -/// -/// Consumer that uses two different generic types from CrossModuleGenericLib, -/// each instantiated with a value type defined in this assembly. -/// Value types are required because CrossModuleCompileable discovery uses -/// CanonicalFormKind.Specific, which preserves value type arguments (unlike -/// reference types which become __Canon, losing the alternate location info). -/// -/// GenericWrapperA<LocalStruct>.InvokeGetValue() and GenericWrapperB<LocalStruct>.InvokeGetValue() -/// are two distinct MethodDefs that each inline Utility.GetValue(), producing -/// multiple cross-module inliner entries for the same inlinee in CrossModuleInlineInfo. -/// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. public struct LocalStruct { public int Value; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs index ec1264f22f1380..cc5c3310d12e5d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs @@ -1,6 +1,6 @@ -// Test: Non-composite consumer of assemblies that were also compiled as composite. -// Step 1 compiles MultiStepLibA + MultiStepLibB as composite. -// Step 2 compiles this assembly non-composite with --ref to LibA and --opt-cross-module. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs index 25bac820fc3002..70f65c1efe2467 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs @@ -1,6 +1,6 @@ -// Test: Transitive cross-module references -// Validates that when InlineableLibTransitive is inlined, its references to ExternalLib -// are properly encoded in the R2R image (requiring tokens for both libraries). +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs index cc1853b48a5bb7..3538a25f0ec8f1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs @@ -1,6 +1,6 @@ -// Test: Cross-module async method inlining -// Validates that cross-module async compilation produces manifest refs -// and [ASYNC] variants for methods calling into a dependency library. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs index b4e2f87ecda45d..762b7d66341144 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs @@ -1,6 +1,6 @@ -// Test: Non-composite runtime-async cross-module inlining with continuation layouts. -// The dependency methods capture GC refs across await points. -// Validates manifest refs and [ASYNC] variants for cross-module async calls. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs index 6170e4ac67360a..0bdcf16e24ab10 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs @@ -1,6 +1,6 @@ -// Test: Async virtual method devirtualization in R2R -// Validates that async methods on sealed/interface types -// produce [ASYNC] variant entries in the R2R image. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs index 28ddb07949521c..a5b92107b19d6d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs @@ -1,6 +1,6 @@ -// Test: Async method without yields (no suspension point) -// When a runtime-async method never actually awaits, crossgen2 may -// omit the resumption stub. This tests that edge case. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs index e92cd6f1ae2de4..b14871c8a4a1b8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs @@ -1,6 +1,6 @@ -// Test: Non-composite runtime-async transitive cross-module inlining. -// Chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. -// Validates transitive manifest refs and async cross-module inlining. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs index 29a8fc879ec463..056a179142d5f8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs @@ -1,5 +1,6 @@ -// Test: Async method that captures GC refs across await -// This forces the compiler to emit a ContinuationLayout fixup. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs index 94a314a1b101fb..45b2bd62a237f2 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs @@ -1,6 +1,6 @@ -// Test: Basic async method emission in R2R -// Validates that runtime-async methods produce [ASYNC] variant entries -// in the R2R image. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs index 2419d72ac82af4..289d3a4550abe2 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs @@ -1,6 +1,6 @@ -// Test: Composite mode async with continuation layouts and resumption stubs. -// Calls async methods from AsyncCompositeContLib that capture GC refs across -// await points, exercising composite-mode ContinuationLayout and RESUME stub emission. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs index 62a06bc816f0c2..eb14f14ba7a369 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs @@ -1,6 +1,6 @@ -// Test: Composite mode async devirtualization across module boundaries. -// Interface defined in AsyncInterfaceLib, call sites here. -// In composite mode, crossgen2 should devirtualize sealed type dispatch. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs index 8dbd03c5de1fd1..e9d867f95d68e3 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncGenericTypesMain.cs @@ -1,12 +1,6 @@ -// Test: Composite-mode async thunk emission for async methods on generic types -// (and generic async methods on generic types). -// -// Regression coverage for the MethodWithToken/OwningType handling described in -// the parent PR ("Enable compilation of async thunks in composite mode") and -// for the follow-up "Get IL for the (possibly instantiated) method, not the -// definition" fix in ReadyToRunCodegenCompilation.cs. Both code paths only run -// for instantiated methods on instantiated generic types compiled inside a -// composite image. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs index 85ff77178e7502..87b2e283d8b190 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs @@ -1,7 +1,6 @@ -// Dependency library for composite async continuation tests. -// Contains runtime-async methods that capture GC refs across await points. -// Used in composite mode to exercise MutableModule token encoding for -// cross-module async continuation layouts and resumption stubs. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs index bb10453f70b05e..d8c320ab105453 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs @@ -1,5 +1,6 @@ -// Dependency library for async cross-module tests. -// Contains runtime-async methods that should be inlineable. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs index b4124b80008fe1..17fd5095957160 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs @@ -1,6 +1,6 @@ -// Dependency library for async cross-module continuation tests. -// Contains runtime-async methods that capture GC refs across await points, -// forcing ContinuationLayout fixup emission when cross-module inlined. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs index f45db23f004570..cf7ab950aff494 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs @@ -1,5 +1,6 @@ -// External types for async transitive cross-module tests. -// Similar to ExternalLib but with async-friendly types. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; public static class AsyncExternalLib diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs index cfd058d733c3ea..a0cd495310aba7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncGenericTypeLib.cs @@ -1,10 +1,6 @@ -// Dependency library for composite-mode generics-on-async-thunks regression tests. -// Provides: -// - GenericContainer: a generic type with both a non-generic and a generic -// async method. The generic-method-on-generic-type combination is the exact -// scenario called out by the parent PR's description as the case that -// originally broke MethodWithToken/OwningType resolution when emitting async -// thunks in composite mode. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs index 528162b5d10c10..60a1632f045e18 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs @@ -1,5 +1,6 @@ -// Dependency library: defines an async interface and sealed implementation -// for cross-module async devirtualization tests in composite mode. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs index 717040a417c362..0d24d799d9055d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs @@ -1,5 +1,6 @@ -// Middle library in async transitive chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. -// Contains runtime-async methods that reference types from AsyncExternalLib. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; From 22d2b51b1b0b042e8937173dbe4d9402e5c88ad4 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:32:26 -0700 Subject: [PATCH 57/74] Restore original headers on R2R test fixtures unchanged by this branch The previous commit replaced ad-hoc top-of-file comments with the standard MIT license header on every test fixture under TestCases/. Limit that header swap to only the 6 fixtures actually added or modified on this branch, and restore the originals on the 29 fixtures that exist unchanged on main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/CrossModuleInlining/AsyncMethods.cs | 6 +++--- .../TestCases/CrossModuleInlining/BasicInlining.cs | 6 +++--- .../TestCases/CrossModuleInlining/CompositeAsync.cs | 5 ++--- .../TestCases/CrossModuleInlining/CompositeBasic.cs | 6 +++--- .../Dependencies/AsyncCompositeLib.cs | 5 ++--- .../Dependencies/AsyncInlineableLib.cs | 3 --- .../Dependencies/CompositeLib.cs | 3 --- .../Dependencies/CrossModuleGenericLib.cs | 3 --- .../CrossModuleInlining/Dependencies/ExternalLib.cs | 3 --- .../Dependencies/InlineableLib.cs | 3 --- .../Dependencies/InlineableLibTransitive.cs | 3 --- .../Dependencies/MultiStepLibA.cs | 5 ++--- .../Dependencies/MultiStepLibB.cs | 5 ++--- .../CrossModuleInlining/MultiInlinerConsumer.cs | 13 +++++++++++-- .../CrossModuleInlining/MultiStepConsumer.cs | 6 +++--- .../CrossModuleInlining/TransitiveReferences.cs | 6 +++--- .../TestCases/RuntimeAsync/AsyncCrossModule.cs | 6 +++--- .../RuntimeAsync/AsyncCrossModuleContinuation.cs | 6 +++--- .../TestCases/RuntimeAsync/AsyncDevirtualize.cs | 6 +++--- .../TestCases/RuntimeAsync/AsyncNoYield.cs | 6 +++--- .../TestCases/RuntimeAsync/AsyncTransitiveMain.cs | 6 +++--- .../TestCases/RuntimeAsync/AsyncWithContinuation.cs | 5 ++--- .../TestCases/RuntimeAsync/BasicAsyncEmission.cs | 6 +++--- .../RuntimeAsync/CompositeAsyncDevirtMain.cs | 6 +++--- .../RuntimeAsync/Dependencies/AsyncDepLib.cs | 5 ++--- .../Dependencies/AsyncDepLibContinuation.cs | 6 +++--- .../RuntimeAsync/Dependencies/AsyncExternalLib.cs | 5 ++--- .../RuntimeAsync/Dependencies/AsyncInterfaceLib.cs | 5 ++--- .../RuntimeAsync/Dependencies/AsyncTransitiveLib.cs | 5 ++--- 29 files changed, 68 insertions(+), 86 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs index ecc124759fc32e..fdf1b159130b95 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncMethods.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Cross-module inlining of async methods +// Validates that async methods from AsyncInlineableLib are cross-module +// inlined into this assembly with CHECK_IL_BODY fixups. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs index 5781e4d8d0de2b..ba301272868c25 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/BasicInlining.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Basic cross-module inlining +// Validates that crossgen2 with --opt-cross-module produces CHECK_IL_BODY fixups +// for methods inlined from InlineableLib into this main assembly. using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs index 08409268fb4d9c..7e19cc7eea200c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeAsync.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Composite mode with runtime-async methods across assemblies. +// Validates that async methods produce [ASYNC] variants in composite output. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs index d71478cea4ff4b..0ba24d5563555c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/CompositeBasic.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Composite mode basic compilation +// Validates that composite mode R2R compilation with multiple assemblies +// produces correct manifest references and component assembly entries. using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs index 9d8a6782d4e3f8..bece15b74b11b6 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCompositeLib.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Dependency library for composite async tests. +// Contains runtime-async methods called from another assembly in composite mode. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs index 1cfd143829072c..153804e6279fc1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncInlineableLib.cs @@ -1,6 +1,3 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs index c0b3a828697b4f..2dc5db2de38bcc 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CompositeLib.cs @@ -1,6 +1,3 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - using System; public static class CompositeLib diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs index c354a0fc7c587d..b17643fb9eb99b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/CrossModuleGenericLib.cs @@ -1,6 +1,3 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - using System.Runtime.CompilerServices; /// diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs index 9fc951a482e107..d56f2880564a13 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/ExternalLib.cs @@ -1,6 +1,3 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - using System; public static class ExternalLib diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs index a6834e200ffec4..a799cfed7282e8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLib.cs @@ -1,6 +1,3 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs index 5412a828c8582f..15fd29dda19d4b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/InlineableLibTransitive.cs @@ -1,6 +1,3 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs index d9785e920d9f1e..c47dd6ac274f28 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibA.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Dependency library for multi-step compilation tests. +// Contains sync inlineable methods used in both composite and non-composite steps. using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs index 7e839815af04b1..cf8acd54d8bd9b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/MultiStepLibB.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Second library for multi-step composite compilation. +// Compiled together with MultiStepLibA as a composite in step 1. using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs index 3956103fe15587..56e9c37d33b7f5 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiInlinerConsumer.cs @@ -1,5 +1,14 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +/// +/// Consumer that uses two different generic types from CrossModuleGenericLib, +/// each instantiated with a value type defined in this assembly. +/// Value types are required because CrossModuleCompileable discovery uses +/// CanonicalFormKind.Specific, which preserves value type arguments (unlike +/// reference types which become __Canon, losing the alternate location info). +/// +/// GenericWrapperA<LocalStruct>.InvokeGetValue() and GenericWrapperB<LocalStruct>.InvokeGetValue() +/// are two distinct MethodDefs that each inline Utility.GetValue(), producing +/// multiple cross-module inliner entries for the same inlinee in CrossModuleInlineInfo. +/// public struct LocalStruct { public int Value; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs index cc5c3310d12e5d..ec1264f22f1380 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/MultiStepConsumer.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Non-composite consumer of assemblies that were also compiled as composite. +// Step 1 compiles MultiStepLibA + MultiStepLibB as composite. +// Step 2 compiles this assembly non-composite with --ref to LibA and --opt-cross-module. using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs index 70f65c1efe2467..25bac820fc3002 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/TransitiveReferences.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Transitive cross-module references +// Validates that when InlineableLibTransitive is inlined, its references to ExternalLib +// are properly encoded in the R2R image (requiring tokens for both libraries). using System; using System.Runtime.CompilerServices; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs index 3538a25f0ec8f1..cc1853b48a5bb7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModule.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Cross-module async method inlining +// Validates that cross-module async compilation produces manifest refs +// and [ASYNC] variants for methods calling into a dependency library. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs index 762b7d66341144..b4e2f87ecda45d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncCrossModuleContinuation.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Non-composite runtime-async cross-module inlining with continuation layouts. +// The dependency methods capture GC refs across await points. +// Validates manifest refs and [ASYNC] variants for cross-module async calls. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs index 0bdcf16e24ab10..6170e4ac67360a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncDevirtualize.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Async virtual method devirtualization in R2R +// Validates that async methods on sealed/interface types +// produce [ASYNC] variant entries in the R2R image. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs index a5b92107b19d6d..28ddb07949521c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncNoYield.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Async method without yields (no suspension point) +// When a runtime-async method never actually awaits, crossgen2 may +// omit the resumption stub. This tests that edge case. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs index b14871c8a4a1b8..e92cd6f1ae2de4 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncTransitiveMain.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Non-composite runtime-async transitive cross-module inlining. +// Chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. +// Validates transitive manifest refs and async cross-module inlining. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs index 056a179142d5f8..29a8fc879ec463 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncWithContinuation.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Async method that captures GC refs across await +// This forces the compiler to emit a ContinuationLayout fixup. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs index 45b2bd62a237f2..94a314a1b101fb 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/BasicAsyncEmission.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Basic async method emission in R2R +// Validates that runtime-async methods produce [ASYNC] variant entries +// in the R2R image. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs index eb14f14ba7a369..62a06bc816f0c2 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncDevirtMain.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Test: Composite mode async devirtualization across module boundaries. +// Interface defined in AsyncInterfaceLib, call sites here. +// In composite mode, crossgen2 should devirtualize sealed type dispatch. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs index d8c320ab105453..bb10453f70b05e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLib.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Dependency library for async cross-module tests. +// Contains runtime-async methods that should be inlineable. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs index 17fd5095957160..b4124b80008fe1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncDepLibContinuation.cs @@ -1,6 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Dependency library for async cross-module continuation tests. +// Contains runtime-async methods that capture GC refs across await points, +// forcing ContinuationLayout fixup emission when cross-module inlined. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs index cf7ab950aff494..f45db23f004570 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncExternalLib.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// External types for async transitive cross-module tests. +// Similar to ExternalLib but with async-friendly types. using System; public static class AsyncExternalLib diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs index 60a1632f045e18..528162b5d10c10 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncInterfaceLib.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Dependency library: defines an async interface and sealed implementation +// for cross-module async devirtualization tests in composite mode. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs index 0d24d799d9055d..717040a417c362 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncTransitiveLib.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - +// Middle library in async transitive chain: AsyncTransitiveMain → AsyncTransitiveLib → AsyncExternalLib. +// Contains runtime-async methods that reference types from AsyncExternalLib. using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; From 0b97456153529387ae776329b3333cabc6a1fa6b Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:20:04 -0700 Subject: [PATCH 58/74] Remove redundant OwningType comparison in MethodWithToken.CompareTo The block at the end of CompareTo compared OwningTypeNotDerivedFromToken and OwningType a second time after the same comparisons earlier in the method, making it unreachable. Drop it and keep the single comparison path. Addresses PR #126904 review comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 2bf566e770cca1..8b79503b6891eb 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -415,21 +415,9 @@ public int CompareTo(MethodWithToken other, TypeSystemComparer comparer) if (result != 0) return result; - // The OwningType/OwningTypeNotDerivedFromToken should be equivalent if the above conditions are equal. result = OwningTypeNotDerivedFromToken.CompareTo(other.OwningTypeNotDerivedFromToken); if (result != 0) return result; - result = comparer.Compare(OwningType, other.OwningType); - if (result != 0) - return result; - - if (OwningTypeNotDerivedFromToken != other.OwningTypeNotDerivedFromToken) - { - if (OwningTypeNotDerivedFromToken) - return 1; - else - return -1; - } return comparer.Compare(OwningType, other.OwningType); } From 4251106f966b916054bebb46c0af965fee5cd8ee Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:23:15 -0700 Subject: [PATCH 59/74] Clarify EnsureDefTokensAreAvailable XML docs The previous wording said tokens are ensured to be present in the MutableModule, but AddTokenToMutableModule short-circuits when the ModuleTokenResolver can already resolve a token from any module known to it. Reword to reflect that a manifest token is added only when no existing token is resolvable. Addresses PR #126904 review comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Compiler/ExternalReferenceTokenManager.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs index 0c86a5754f3f9e..d5cb890f6aa9e4 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs @@ -26,8 +26,9 @@ public ExternalReferenceTokenManager(MutableModule mutableModule, ModuleTokenRes } /// - /// Ensures that all the tokens necessary for creating a ReadyToRun signature for the given entity are present in the MutableModule. - /// Adds the necessary tokens for the given entity to the MutableModule if they are not already present. + /// Ensures that all the tokens necessary for creating a ReadyToRun signature for the given entities are available. + /// If a token for an entity is not already resolvable from a module known to the , + /// a new token is added to the manifest . /// public void EnsureDefTokensAreAvailable(IEnumerable entities, ModuleDesc moduleForNewReferences, bool referencesAreForAsyncMethod) { @@ -48,8 +49,9 @@ public void EnsureDefTokensAreAvailable(IEnumerable entities, } /// - /// Ensures that all the tokens necessary for creating a ReadyToRun signature for the given entity are present in the MutableModule. - /// Adds the necessary tokens for the given entity to the MutableModule if they are not already present. + /// Ensures that all the tokens necessary for creating a ReadyToRun signature for the given entity are available. + /// If a token for the entity is not already resolvable from a module known to the , + /// a new token is added to the manifest . /// public void EnsureDefTokensAreAvailable(TypeSystemEntity entity, ModuleDesc moduleForNewReferences, bool referencesAreForAsyncMethod) { From 4f380b53d12849333d60ced0e50a553a0b332dca Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:00:13 -0700 Subject: [PATCH 60/74] Add positive composite cross-module inlining test Composite mode does produce a CrossModuleInlineInfo section when an inlineable method comes from an assembly outside the version bubble (passed as a Reference with --opt-cross-module). This complements CompositeDoesNotProduceCrossModuleInliningInfo, which covers the case where all inlinees are composite inputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 1816bdd4511027..9daf07d80b06b8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -469,6 +469,61 @@ static void Validate(ReadyToRunReader reader) } } + /// + /// Positive complement to : + /// composite mode DOES produce a CrossModuleInlineInfo section when an inlineable method + /// comes from an assembly outside the version bubble (passed as a Reference with + /// --opt-cross-module). Crossgen2 only treats modules NOT in the version bubble as + /// cross-module inlineable, so an external Reference is required to exercise this. + /// + [Fact] + public void CompositeProducesCrossModuleInliningInfoForExternalReference() + { + var inlineableLib = new CompiledAssembly + { + AssemblyName = "InlineableLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/InlineableLib.cs"], + }; + var compositeLib = new CompiledAssembly + { + AssemblyName = "CompositeLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/CompositeLib.cs"], + }; + var compositeMain = new CompiledAssembly + { + AssemblyName = nameof(CompositeProducesCrossModuleInliningInfoForExternalReference), + SourceResourceNames = ["CrossModuleInlining/BasicInlining.cs"], + References = [inlineableLib] + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(CompositeProducesCrossModuleInliningInfoForExternalReference), + [ + new(nameof(CompositeProducesCrossModuleInliningInfoForExternalReference), + [ + new CrossgenAssembly(inlineableLib) + { + Kind = Crossgen2InputKind.Reference, + Options = [Crossgen2AssemblyOption.CrossModuleOptimization], + }, + new CrossgenAssembly(compositeLib), + new CrossgenAssembly(compositeMain), + ]) + { + Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "InlineableLib", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInlinedMethod(reader, "TestGetValue", "GetValue", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInliningInfo(reader, out diag), diag); + } + } + /// /// Composite mode with runtime-async methods in both assemblies. /// Validates async variants exist in composite output. From faa8ce169da95b6156ca3ea9ac696a2c4eb1dce2 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:03:24 -0700 Subject: [PATCH 61/74] Simplify test comments, remove redundant test Composite mode and the emission of Async methods are fairly orthogonal and don't need tests just validation the existence of async method. --- .../TestCases/R2RTestSuites.cs | 115 ++---------------- .../CompositeAsyncContinuationMain.cs | 30 ----- .../Dependencies/AsyncCompositeContLib.cs | 27 ---- 3 files changed, 12 insertions(+), 160 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 9daf07d80b06b8..63c2f7b87ff112 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -417,16 +417,8 @@ static void Validate(ReadyToRunReader reader) /// /// Negative test: a composite image whose only inputs are the inlinee and the inliner - /// does NOT produce a CrossModuleInlineInfo section. CrossModuleInlineInfo only records - /// inlining where the inlinee module is OUTSIDE the compiled image's version bubble - /// (typically added via --opt-cross-module on a reference assembly). Here both - /// modules are composite inputs and therefore in the same version bubble, so any - /// inlining between them is recorded in the per-module InliningInfo2 section instead. - /// A different setup — composite output plus an external reference passed via - /// --opt-cross-module — could still produce CrossModuleInlineInfo entries; this - /// test only covers the "all inlinees are composite inputs" case. - /// Compare with , which uses the same source modules - /// in a non-composite layout and DOES produce CrossModuleInlineInfo entries. + /// assemblies does NOT produce a CrossModuleInlineInfo section. CrossModuleInlineInfo only records + /// inlining where the inlinee module is outside the compiled image's version bubble /// [Fact] public void CompositeDoesNotProduceCrossModuleInliningInfo() @@ -471,10 +463,11 @@ static void Validate(ReadyToRunReader reader) /// /// Positive complement to : - /// composite mode DOES produce a CrossModuleInlineInfo section when an inlineable method + /// composite mode produces a CrossModuleInlineInfo section when an inlineable method /// comes from an assembly outside the version bubble (passed as a Reference with - /// --opt-cross-module). Crossgen2 only treats modules NOT in the version bubble as - /// cross-module inlineable, so an external Reference is required to exercise this. + /// --opt-cross-module). Crossgen2 only treats modules not in the version bubble as + /// "cross-module inlined" (everything is intra-module in composite), so an external + /// Reference is required to exercise this. /// [Fact] public void CompositeProducesCrossModuleInliningInfoForExternalReference() @@ -568,22 +561,10 @@ static void Validate(ReadyToRunReader reader) } } - /// - /// The full intersection: composite + runtime-async + cross-module inlining. - /// Async methods from AsyncCompositeLib are inlined into CompositeAsyncMain - /// within a composite image, exercising MutableModule token encoding for - /// cross-module async continuation layouts. - /// /// /// Composite + runtime-async + intra-bubble inlining matrix test. /// Verifies that, in composite mode, awaitless async candidates ARE inlined into - /// their callers (recorded in InliningInfo2 — composite inputs share a version - /// bubble, so true CrossModuleInlineInfo entries are not produced for them), while - /// candidates whose body contains a real await are NOT inlined. The latter - /// is a JIT-level limitation: Compiler::impSetupAsyncCall in - /// importercalls.cpp issues a FATAL CALLEE_AWAIT observation as soon as it - /// sees an async call inside an inlining candidate (see also CALLEE_ASYNC_SUSPEND). - /// The matrix covers Task, Task<primitive>, and Task<class> return shapes. + /// their callers. /// [Fact] public void CompositeAsyncInliningMatrix() @@ -634,83 +615,11 @@ static void Validate(ReadyToRunReader reader) } /// - /// Composite mode with runtime-async methods that capture GC refs across await - /// points, exercising ContinuationLayout and RESUME stub emission in composite images. - /// Validates that both the library and main assembly's async methods produce - /// [ASYNC] variants, [RESUME] stubs, and ContinuationLayout fixups. - /// - [Fact] - public void CompositeAsyncContinuationAndResume() - { - var asyncCompositeContLib = new CompiledAssembly - { - AssemblyName = "AsyncCompositeContLib", - SourceResourceNames = - [ - "RuntimeAsync/Dependencies/AsyncCompositeContLib.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - }; - var compositeAsyncContMain = new CompiledAssembly - { - AssemblyName = "CompositeAsyncContinuationMain", - SourceResourceNames = - [ - "RuntimeAsync/CompositeAsyncContinuationMain.cs", - "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", - ], - Features = { RuntimeAsyncFeature }, - References = [asyncCompositeContLib] - }; - - new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(CompositeAsyncContinuationAndResume), - [ - new(nameof(CompositeAsyncContinuationAndResume), - [ - new CrossgenAssembly(asyncCompositeContLib), - new CrossgenAssembly(compositeAsyncContMain), - ]) - { - Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize], - Validate = Validate, - }, - ])); - - static void Validate(ReadyToRunReader reader) - { - string diag; - // Library methods produce async variants and resume stubs - Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureRefComposite", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "CaptureArrayComposite", out diag), diag); - Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureRefComposite", out diag), diag); - Assert.True(R2RAssert.HasResumptionStub(reader, "CaptureArrayComposite", out diag), diag); - - // Main assembly methods produce async variants and resume stubs - Assert.True(R2RAssert.HasAsyncVariant(reader, "CallCaptureRefComposite", out diag), diag); - Assert.True(R2RAssert.HasAsyncVariant(reader, "LocalCaptureAcrossAwait", out diag), diag); - Assert.True(R2RAssert.HasResumptionStub(reader, "CallCaptureRefComposite", out diag), diag); - Assert.True(R2RAssert.HasResumptionStub(reader, "LocalCaptureAcrossAwait", out diag), diag); - - // ContinuationLayout fixups are present for methods with GC refs across awaits - Assert.True(R2RAssert.HasContinuationLayout(reader, "CaptureRefComposite", out diag), diag); - Assert.True(R2RAssert.HasContinuationLayout(reader, "LocalCaptureAcrossAwait", out diag), diag); - } - } - - /// - /// Composite-mode regression coverage for async thunk emission of methods on - /// generic types (and generic methods on generic types). The parent PR's - /// description specifically calls these out as the case that originally - /// broke MethodWithToken..ctor() owning-type computation when the - /// async-thunk ILStub forced a strip-instantiation in - /// CorInfoImpl.HandleToModuleToken. The follow-up "Get IL for the - /// (possibly instantiated) method, not the definition" fix in - /// ReadyToRunCodegenCompilation.EnsureAsyncThunkTokensAreAvailable - /// is also exercised by this test (it only matters for instantiated - /// methods/types). Both reference-type and value-type instantiations are - /// covered because token resolution differs between the two. + /// Validate that async thunks with generic owning types are correctly emitted in composite mode. + /// Async thunks (and all "faux" method IL stubs) strip the instantiation away when constructing a MethodWithToken. + /// This is fine for the Method instantiation, but the Type instantiation needs to be tracked properly. + /// https://github.com/dotnet/runtime/pull/126904 added support for ensuring the OwningType signature modifier is emitted + /// for these methods. /// [Fact] public void CompositeAsyncGenericTypes() diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs deleted file mode 100644 index 289d3a4550abe2..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/CompositeAsyncContinuationMain.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class CompositeAsyncContinuationMain -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCaptureRefComposite() - { - return await AsyncCompositeContLib.CaptureRefComposite(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CallCaptureArrayComposite() - { - return await AsyncCompositeContLib.CaptureArrayComposite(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task LocalCaptureAcrossAwait() - { - object o = new object(); - string s = "local"; - await Task.Yield(); - return s.Length + o.GetHashCode(); - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs deleted file mode 100644 index 87b2e283d8b190..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/Dependencies/AsyncCompositeContLib.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -public static class AsyncCompositeContLib -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CaptureRefComposite() - { - object o = new object(); - string s = "composite_ref"; - await Task.Yield(); - return s + o.GetHashCode(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public static async Task CaptureArrayComposite() - { - int[] arr = new int[] { 5, 10, 15 }; - string label = "total"; - await Task.Yield(); - return arr[0] + arr[1] + label.Length; - } -} From b85e2d257ce3b57e7eed7e7e7671e752d9e59b3f Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:32:56 -0700 Subject: [PATCH 62/74] Refine documentation for CompositeProducesCrossModuleInliningInfoForExternalReference test --- .../ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 63c2f7b87ff112..78bdb5535f9241 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -465,9 +465,7 @@ static void Validate(ReadyToRunReader reader) /// Positive complement to : /// composite mode produces a CrossModuleInlineInfo section when an inlineable method /// comes from an assembly outside the version bubble (passed as a Reference with - /// --opt-cross-module). Crossgen2 only treats modules not in the version bubble as - /// "cross-module inlined" (everything is intra-module in composite), so an external - /// Reference is required to exercise this. + /// --opt-cross-module). /// [Fact] public void CompositeProducesCrossModuleInliningInfoForExternalReference() From 37d3d6a16ca772ac31dce8f14fbeb6c227fcaa37 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:34:44 -0700 Subject: [PATCH 63/74] Include OwningTypeNotDerivedFromToken in FieldWithToken.Equals The new forceOwningTypeNotDerivedFromToken constructor parameter affects signature emission via OwningTypeNotDerivedFromToken, but equality previously only compared Field and Token. Since FieldWithToken is used as a cache key (e.g., NodeCache), instances differing only by this flag could incorrectly share cached signatures. Mirrors the existing MethodWithToken.Equals behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 8b79503b6891eb..dc90fcf26d310b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -97,7 +97,9 @@ public bool Equals(FieldWithToken fieldWithToken) if (fieldWithToken == null) return false; - return Field == fieldWithToken.Field && Token.Equals(fieldWithToken.Token); + return Field == fieldWithToken.Field + && Token.Equals(fieldWithToken.Token) + && OwningTypeNotDerivedFromToken == fieldWithToken.OwningTypeNotDerivedFromToken; } public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) From 04352adec8562a406b9d373737d97bbfa76a14d5 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:39:24 -0700 Subject: [PATCH 64/74] Forward strippedInstantiation to FieldWithToken in shared generic lookup The MethodDesc branch already forwards strippedInstantiation as forceOwningTypeFromMethodDesc; mirror that for the FieldDesc branch so that faux-IL stripped-instantiation tokens correctly mark OwningTypeNotDerivedFromToken rather than discarding the flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index dc90fcf26d310b..fedcc5216f2fc3 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -2758,7 +2758,8 @@ private void ComputeRuntimeLookupForSharedGenericToken( } else if (helperArg is FieldDesc fieldDesc) { - helperArg = new FieldWithToken(fieldDesc, HandleToModuleToken(ref pResolvedToken, out _)); + ModuleToken fieldToken = HandleToModuleToken(ref pResolvedToken, out bool strippedInstantiation); + helperArg = new FieldWithToken(fieldDesc, fieldToken, forceOwningTypeNotDerivedFromToken: strippedInstantiation); } var methodContext = new GenericContext(callerHandle); From 47e4e256d136ee05b7dab631cb31c64ce1c95fe2 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:44:46 -0700 Subject: [PATCH 65/74] Assert strippedInstantiation is false in FieldDesc helper-arg path The FieldDesc branch of ComputeRuntimeLookupForSharedGenericToken is not expected to encounter stripped instantiations from faux IL. Add a Debug.Assert to catch any future regression while keeping the forwarding to FieldWithToken as a defensive fallback for retail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index fedcc5216f2fc3..b349648c4fc5b3 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -2759,6 +2759,7 @@ private void ComputeRuntimeLookupForSharedGenericToken( else if (helperArg is FieldDesc fieldDesc) { ModuleToken fieldToken = HandleToModuleToken(ref pResolvedToken, out bool strippedInstantiation); + Debug.Assert(!strippedInstantiation); helperArg = new FieldWithToken(fieldDesc, fieldToken, forceOwningTypeNotDerivedFromToken: strippedInstantiation); } From 99b84bca131b0cea1f655c3b5f9fc4fabb06dbd5 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:52:45 -0700 Subject: [PATCH 66/74] Fix FieldWithToken.CompareTo --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index b349648c4fc5b3..325962ec8cdcf0 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -127,8 +127,10 @@ public int CompareTo(FieldWithToken other, TypeSystemComparer comparer) result = comparer.Compare(Field, other.Field); if (result != 0) return result; - - return Token.CompareTo(other.Token); + result = Token.CompareTo(other.Token); + if (result != 0) + return result; + return OwningTypeNotDerivedFromToken.CompareTo(other.OwningTypeNotDerivedFromToken); } } From 7f15da0af415ce503aad7bb6c95921be71028043 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:01:47 -0700 Subject: [PATCH 67/74] Add asserts, revert extra change --- .../Compiler/ExternalReferenceTokenManager.cs | 5 ++++- src/tests/Common/tests.targets | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs index d5cb890f6aa9e4..fd1fd6ae60784f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs @@ -32,6 +32,8 @@ public ExternalReferenceTokenManager(MutableModule mutableModule, ModuleTokenRes /// public void EnsureDefTokensAreAvailable(IEnumerable entities, ModuleDesc moduleForNewReferences, bool referencesAreForAsyncMethod) { + Debug.Assert(_mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences == null); + Debug.Assert(!_mutableModule.CreatingTokensForAsyncMethod); _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = moduleForNewReferences; _mutableModule.CreatingTokensForAsyncMethod = referencesAreForAsyncMethod; try @@ -55,7 +57,8 @@ public void EnsureDefTokensAreAvailable(IEnumerable entities, /// public void EnsureDefTokensAreAvailable(TypeSystemEntity entity, ModuleDesc moduleForNewReferences, bool referencesAreForAsyncMethod) { - + Debug.Assert(_mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences == null); + Debug.Assert(!_mutableModule.CreatingTokensForAsyncMethod); _mutableModule.ModuleThatIsCurrentlyTheSourceOfNewReferences = moduleForNewReferences; _mutableModule.CreatingTokensForAsyncMethod = referencesAreForAsyncMethod; try diff --git a/src/tests/Common/tests.targets b/src/tests/Common/tests.targets index dd90fa598beeea..335728c416ecb5 100644 --- a/src/tests/Common/tests.targets +++ b/src/tests/Common/tests.targets @@ -222,10 +222,10 @@ <_StaleIlCg2DoneFile Include="@(_IlCg2DoneFiles)" - Condition="'@(_IlCg2DoneFiles)' != '' and ($([System.IO.File]::GetLastWriteTime('%(Identity)').Ticks) < $(_Crossgen2InCoreRootTime))" /> + Condition="$([System.IO.File]::GetLastWriteTime('%(Identity)').Ticks) < $(_Crossgen2InCoreRootTime)" /> - From 81dac5f5e3ebba63615997617c78f8e1f83a265a Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:48:11 -0700 Subject: [PATCH 68/74] Remove redundant CreatingTokensForAsyncMethod set in EnsureAsyncThunkTokensAreAvailable ExternalReferenceTokenManager.EnsureDefTokensAreAvailable now manages this flag itself when referencesAreForAsyncMethod is true, so the manual try/finally in EnsureAsyncThunkTokensAreAvailable is redundant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Compiler/ReadyToRunCodegenCompilation.cs | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs index 969d7a6c8cc9ce..a56e1f90e8d67e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs @@ -794,43 +794,35 @@ void EnsureAsyncThunkTokensAreAvailable(MethodDesc method) { return; } - try + var bytes = il.GetILBytes(); + // Use ILTokenReplacer to iterate over tokens, not actually replace them + ILTokenReplacer.Replace(bytes, tok => { - NodeFactory.ManifestMetadataTable._mutableModule.CreatingTokensForAsyncMethod = true; - var bytes = il.GetILBytes(); - // Use ILTokenReplacer to iterate over tokens, not actually replace them - ILTokenReplacer.Replace(bytes, tok => - { - switch(il.GetObject(tok)) - { - case TypeSystemEntity tse: - _tokenManager.EnsureDefTokensAreAvailable(tse, ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); - break; - default: - // We don't need to worry about string handles - break; - } - return tok; - }); - // ILTokenReplacer doesn't handle exception regions or local variable types, so handle those separately - var exceptionRegions = (ILExceptionRegion[])il.GetExceptionRegions(); - for (int i = 0; i < exceptionRegions.Length; i++) + switch(il.GetObject(tok)) { - var region = exceptionRegions[i]; - if (region.Kind == ILExceptionRegionKind.Catch) - { - TypeSystemEntity catchType = (TypeSystemEntity)il.GetObject(region.ClassToken); - _tokenManager.EnsureDefTokensAreAvailable(catchType, ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); - } + case TypeSystemEntity tse: + _tokenManager.EnsureDefTokensAreAvailable(tse, ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); + break; + default: + // We don't need to worry about string handles + break; } - foreach (var local in il.GetLocals()) + return tok; + }); + // ILTokenReplacer doesn't handle exception regions or local variable types, so handle those separately + var exceptionRegions = (ILExceptionRegion[])il.GetExceptionRegions(); + for (int i = 0; i < exceptionRegions.Length; i++) + { + var region = exceptionRegions[i]; + if (region.Kind == ILExceptionRegionKind.Catch) { - _tokenManager.EnsureDefTokensAreAvailable(local.Type, ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); + TypeSystemEntity catchType = (TypeSystemEntity)il.GetObject(region.ClassToken); + _tokenManager.EnsureDefTokensAreAvailable(catchType, ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); } } - finally + foreach (var local in il.GetLocals()) { - NodeFactory.ManifestMetadataTable._mutableModule.CreatingTokensForAsyncMethod = false; + _tokenManager.EnsureDefTokensAreAvailable(local.Type, ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module, true); } } From a902c8938a6e12585349408364d139f331bec269 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:34:39 +0000 Subject: [PATCH 69/74] Get Field token with compilation ModuleTokenResolver --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 325962ec8cdcf0..3edda099480b33 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -1485,8 +1485,12 @@ private ModuleToken HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToke resultField = resultField.GetTypicalFieldDefinition(); strippedInstantiation = resultField != resultDef; + if(!(_compilation.NodeFactory.CompilationModuleGroup.VersionsWithType(resultField.OwningType) || resultField.OwningType.IsNonVersionable())) + { + ModuleToken result = _compilation.NodeFactory.Resolver.GetModuleTokenForField(resultField, allowDynamicallyCreatedReference: true, throwIfNotFound: true); + return result; + } Debug.Assert(resultField is EcmaField); - Debug.Assert(_compilation.NodeFactory.CompilationModuleGroup.VersionsWithType(resultField.OwningType) || resultField.OwningType.IsNonVersionable()); token = (mdToken)MetadataTokens.GetToken(((EcmaField)resultField).Handle); module = ((EcmaField)resultField).Module; } From 9a6f3f7db003d7816a6063fa1e098ee940c9869b Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:38:24 +0000 Subject: [PATCH 70/74] Use IL availability as determining factor for whether to create MutableModule tokens --- .../Compiler/ReadyToRunCodegenCompilation.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs index 5d4d01f66edeae..93ef4231f0a678 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs @@ -781,15 +781,9 @@ void EnsureAsyncThunkTokensAreAvailable(MethodDesc method) { if (!method.IsCompilerGeneratedILBodyForAsync()) return; - if (method.Signature.ReturnType.IsVoid) - { - // Special method in CoreLib. These are not compiled in R2R. - return; - } var ilProvider = (ReadyToRunILProvider)_methodILCache.ILProvider; MethodIL il = ilProvider.GetMethodIL(method); // We shouldn't get null IL, but just in case, handle it gracefully - Debug.Assert(il is not null); if (il is null) { return; From 8290b2cac139ddaa657bb142aa74732e19155878 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:39:42 +0000 Subject: [PATCH 71/74] Enable getting PrimaryMethodDesc for ReturnDroppingThunks --- .../Common/Compiler/AsyncMethodVariant.cs | 6 ++++++ .../CompilerTypeSystemContext.Async.cs | 20 +++++++++++++++++++ .../TypeSystem/MethodDescExtensions.cs | 4 ++++ 3 files changed, 30 insertions(+) diff --git a/src/coreclr/tools/Common/Compiler/AsyncMethodVariant.cs b/src/coreclr/tools/Common/Compiler/AsyncMethodVariant.cs index 8b68888e29e5f7..78b0c9c76b174b 100644 --- a/src/coreclr/tools/Common/Compiler/AsyncMethodVariant.cs +++ b/src/coreclr/tools/Common/Compiler/AsyncMethodVariant.cs @@ -171,5 +171,11 @@ public static MethodDesc GetTargetOfAsyncVariant(this MethodDesc method) Debug.Assert(method.IsAsyncVariant()); return ((CompilerTypeSystemContext)method.Context).GetTargetOfAsyncVariantMethod(method); } + + public static MethodDesc GetTargetOfReturnDroppingAsyncThunk(this MethodDesc method) + { + Debug.Assert(method.IsReturnDroppingAsyncThunk()); + return ((CompilerTypeSystemContext)method.Context).GetTargetOfReturnDroppingThunk(method); + } } } diff --git a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Async.cs b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Async.cs index e974d78ffb00ca..a1c8177e5a09d4 100644 --- a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Async.cs +++ b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Async.cs @@ -207,6 +207,26 @@ public MethodDesc GetAsyncVariantMethod(MethodDesc taskReturningMethod) return result; } + public MethodDesc GetTargetOfReturnDroppingThunk(MethodDesc returnDroppingThunk) + { + Debug.Assert(returnDroppingThunk.IsReturnDroppingAsyncThunk()); + var returnDroppingThunkDefinition = (ReturnDroppingAsyncThunk)returnDroppingThunk.GetTypicalMethodDefinition(); + MethodDesc result = returnDroppingThunkDefinition.AsyncVariantTarget; + + // If there are generics involved, we need to specialize + if (returnDroppingThunk != returnDroppingThunkDefinition) + { + TypeDesc owningType = returnDroppingThunk.OwningType; + if (owningType != returnDroppingThunkDefinition.OwningType) + result = GetMethodForInstantiatedType(result, (InstantiatedType)owningType); + + if (returnDroppingThunk.HasInstantiation && !returnDroppingThunk.IsMethodDefinition) + result = GetInstantiatedMethod(result, returnDroppingThunk.Instantiation); + } + + return result; + } + private sealed class AsyncVariantHashtable : LockFreeReaderHashtable { protected override int GetKeyHashCode(EcmaMethod key) => key.GetHashCode(); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/TypeSystem/MethodDescExtensions.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/TypeSystem/MethodDescExtensions.cs index 935a160ee4f6ea..8694092b5fd930 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/TypeSystem/MethodDescExtensions.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/TypeSystem/MethodDescExtensions.cs @@ -21,6 +21,10 @@ public static MethodDesc GetPrimaryMethodDesc(this MethodDesc method) { return method.GetUnboxedMethod().GetPrimaryMethodDesc(); } + if (method.IsReturnDroppingAsyncThunk()) + { + return method.GetTargetOfReturnDroppingAsyncThunk().GetPrimaryMethodDesc(); + } return method switch { PInvokeTargetNativeMethod pinvokeTarget => pinvokeTarget.Target, From f38e8daddd0ce07d46ad244ee41e7e569406fcd2 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:14:22 +0000 Subject: [PATCH 72/74] Compare forceOwningTypeNotDerivedFromToken in XWithTokien types --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 3edda099480b33..4337e38b735a19 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -55,11 +55,13 @@ public class FieldWithToken : IEquatable public readonly FieldDesc Field; public readonly ModuleToken Token; public readonly bool OwningTypeNotDerivedFromToken; + private readonly bool _forceOwningTypeNotDerivedFromToken; public FieldWithToken(FieldDesc field, ModuleToken token, bool forceOwningTypeNotDerivedFromToken = false) { Field = field; Token = token; + _forceOwningTypeNotDerivedFromToken = forceOwningTypeNotDerivedFromToken; if (token.TokenType == CorTokenType.mdtMemberRef) { var memberRef = token.MetadataReader.GetMemberReference((MemberReferenceHandle)token.Handle); @@ -75,7 +77,7 @@ public FieldWithToken(FieldDesc field, ModuleToken token, bool forceOwningTypeNo break; } } - if (forceOwningTypeNotDerivedFromToken) + if (_forceOwningTypeNotDerivedFromToken) { OwningTypeNotDerivedFromToken = true; } @@ -99,7 +101,7 @@ public bool Equals(FieldWithToken fieldWithToken) return Field == fieldWithToken.Field && Token.Equals(fieldWithToken.Token) - && OwningTypeNotDerivedFromToken == fieldWithToken.OwningTypeNotDerivedFromToken; + && _forceOwningTypeNotDerivedFromToken == fieldWithToken._forceOwningTypeNotDerivedFromToken; } public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) @@ -130,7 +132,7 @@ public int CompareTo(FieldWithToken other, TypeSystemComparer comparer) result = Token.CompareTo(other.Token); if (result != 0) return result; - return OwningTypeNotDerivedFromToken.CompareTo(other.OwningTypeNotDerivedFromToken); + return _forceOwningTypeNotDerivedFromToken.CompareTo(other._forceOwningTypeNotDerivedFromToken); } } @@ -141,6 +143,7 @@ public class MethodWithToken public readonly TypeDesc ConstrainedType; public readonly bool Unboxing; public readonly bool OwningTypeNotDerivedFromToken; + private readonly bool _forceOwningTypeNotDerivedFromToken; public readonly TypeDesc OwningType; public MethodWithToken(MethodDesc method, ModuleToken token, TypeDesc constrainedType, bool unboxing, TypeSystemEntity genericContextObject, TypeDesc devirtualizedMethodOwner = null, bool forceOwningTypeFromMethodDesc = false) @@ -151,7 +154,8 @@ public MethodWithToken(MethodDesc method, ModuleToken token, TypeDesc constraine Token = token; ConstrainedType = constrainedType; Unboxing = unboxing; - if (!forceOwningTypeFromMethodDesc) + _forceOwningTypeNotDerivedFromToken = forceOwningTypeFromMethodDesc; + if (!_forceOwningTypeNotDerivedFromToken) { OwningType = GetMethodTokenOwningType(this, constrainedType, genericContextObject, devirtualizedMethodOwner, out OwningTypeNotDerivedFromToken); } @@ -343,7 +347,7 @@ public bool Equals(MethodWithToken methodWithToken) && OwningType == methodWithToken.OwningType && ConstrainedType == methodWithToken.ConstrainedType && Unboxing == methodWithToken.Unboxing - && OwningTypeNotDerivedFromToken == methodWithToken.OwningTypeNotDerivedFromToken; + && _forceOwningTypeNotDerivedFromToken == methodWithToken._forceOwningTypeNotDerivedFromToken; return equals; } @@ -419,7 +423,7 @@ public int CompareTo(MethodWithToken other, TypeSystemComparer comparer) if (result != 0) return result; - result = OwningTypeNotDerivedFromToken.CompareTo(other.OwningTypeNotDerivedFromToken); + result = _forceOwningTypeNotDerivedFromToken.CompareTo(other._forceOwningTypeNotDerivedFromToken); if (result != 0) return result; From ad8fa51947bd7f5c2072031c3af17b1d74baa68a Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:21:42 +0100 Subject: [PATCH 73/74] Allow empty string to be resolved for inlinees of ILStubs Co-authored-by: Copilot --- .../MetadataEmitter/TypeSystemMetadataEmitter.cs | 1 + .../JitInterface/CorInfoImpl.ReadyToRun.cs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/coreclr/tools/Common/TypeSystem/MetadataEmitter/TypeSystemMetadataEmitter.cs b/src/coreclr/tools/Common/TypeSystem/MetadataEmitter/TypeSystemMetadataEmitter.cs index d7a87a69fbf2e0..b29223aafd07e6 100644 --- a/src/coreclr/tools/Common/TypeSystem/MetadataEmitter/TypeSystemMetadataEmitter.cs +++ b/src/coreclr/tools/Common/TypeSystem/MetadataEmitter/TypeSystemMetadataEmitter.cs @@ -35,6 +35,7 @@ internal class TypeSystemMetadataEmitter _metadataBuilder = new MetadataBuilder(); _ilBuilder = new BlobBuilder(); _methodBodyStream = new MethodBodyStreamEncoder(_ilBuilder); + _ = _metadataBuilder.GetOrAddString(string.Empty); StringHandle assemblyNameHandle = _metadataBuilder.GetOrAddString(assemblyName.Name); if (assemblyName.CultureName != null) throw new ArgumentException("assemblyName"); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 4337e38b735a19..92738de737625d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -1544,8 +1544,16 @@ private InfoAccessType constructStringLiteral(CORINFO_MODULE_STRUCT_* module, md { MethodILScope methodIL = HandleToObject(module); - // If this is not a MethodIL backed by a physical method body, we need to remap the token. - Debug.Assert(methodIL.GetMethodILScopeDefinition() is IEcmaMethodIL); + Debug.Assert(methodIL.GetMethodILScopeDefinition() is IEcmaMethodIL + // We may be calling this from emptyStringLiteral for a method inlined into a thunk + || metaTok == (mdToken)CorTokenType.mdtString); + if (metaTok == (mdToken)CorTokenType.mdtString) + { + ISymbolNode str = _compilation.SymbolNodeFactory.StringLiteral( + new ModuleToken(_compilation.NodeFactory.ManifestMetadataTable._mutableModule, metaTok)); + ppValue = (void*)ObjectToHandle(str); + return InfoAccessType.IAT_PPVALUE; + } IEcmaModule metadataModule = ((IEcmaMethodIL)methodIL.GetMethodILScopeDefinition()).Module; ISymbolNode stringObject = _compilation.SymbolNodeFactory.StringLiteral( From 620108d25d21281d92479c321157d9a8705f4d20 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 30 Apr 2026 08:56:44 +0100 Subject: [PATCH 74/74] string.Empty is guaranteed to be default(StringHandle), doesn't need to be added --- .../TypeSystem/MetadataEmitter/TypeSystemMetadataEmitter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/coreclr/tools/Common/TypeSystem/MetadataEmitter/TypeSystemMetadataEmitter.cs b/src/coreclr/tools/Common/TypeSystem/MetadataEmitter/TypeSystemMetadataEmitter.cs index b29223aafd07e6..d7a87a69fbf2e0 100644 --- a/src/coreclr/tools/Common/TypeSystem/MetadataEmitter/TypeSystemMetadataEmitter.cs +++ b/src/coreclr/tools/Common/TypeSystem/MetadataEmitter/TypeSystemMetadataEmitter.cs @@ -35,7 +35,6 @@ internal class TypeSystemMetadataEmitter _metadataBuilder = new MetadataBuilder(); _ilBuilder = new BlobBuilder(); _methodBodyStream = new MethodBodyStreamEncoder(_ilBuilder); - _ = _metadataBuilder.GetOrAddString(string.Empty); StringHandle assemblyNameHandle = _metadataBuilder.GetOrAddString(assemblyName.Name); if (assemblyName.CultureName != null) throw new ArgumentException("assemblyName");