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