Skip to content

Commit 9b04b4b

Browse files
techiedesuT-Gro
andauthored
ILVerify: soft comparison ignoring IL byte offset drift (#18090) (#19553)
Add offset-tolerant fallback to ILVerify baseline comparison so that changes which only shift IL byte offsets no longer fail CI. Co-authored-by: Tomas Grosup <Tomas.Grosup@gmail.com>
1 parent fdf96f5 commit 9b04b4b

4 files changed

Lines changed: 181 additions & 12 deletions

File tree

.github/skills/ilverify-failure/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ description: Fix ILVerify baseline failures when IL shape changes (codegen, new
88
## When to Use
99
IL shape changed (codegen, new types, method signatures) and ILVerify CI job fails.
1010

11+
## Offset-Only Differences
12+
Changes that only shift IL byte offsets (`[offset 0x...]`) — for example adding code above an existing error site — are detected automatically and **do not fail CI**. A warning is printed suggesting a baseline update. No action is required, but refreshing baselines keeps them accurate.
13+
1114
## Update Baselines
1215
```bash
1316
TEST_UPDATE_BSL=1 pwsh tests/ILVerify/ilverify.ps1
1417
```
18+
Or use the `/run ilverify` PR comment command to update baselines via CI.
1519

1620
## Baselines Location
1721
`tests/ILVerify/*.bsl`

DEVGUIDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,13 @@ ilverify_FSharp.Core_Release_netstandard2.0.bsl
335335
ilverify_FSharp.Core_Release_netstandard2.1.bsl
336336
```
337337

338+
The comparison uses a two-level approach: an exact match is tried first, then a **soft comparison** that ignores IL byte offsets (`[offset 0x...]`) and trailing whitespace. Offsets shift whenever code above the error site changes, even though the verification error itself is semantically identical. When only offsets differ the check passes with a warning suggesting a baseline update.
339+
338340
If you want to update them, either
339341

340342
1. Run the [ilverify.ps1]([url](https://github.com/dotnet/fsharp/blob/main/tests/ILVerify/ilverify.ps1)) script in PowerShell. The script will create `.actual` files. If the differences make sense, replace the original baselines with the actual files.
341343
2. Set the `TEST_UPDATE_BSL` to `1` (please refer to "Updating baselines in tests" section in this file) **and** run `ilverify.ps1` - this will automatically replace baselines. After that, please carefully review the change and push it to your branch if it makes sense.
344+
3. Use the `/run ilverify` PR comment command to update baselines automatically via CI.
342345

343346
## Automated Source Code Formatting
344347

tests/ILVerify/ilverify.Tests.ps1

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Pester tests for ILVerify helper functions defined in ilverify.ps1.
2+
# Compatible with Pester 3.x+ (ships with Windows PowerShell).
3+
# Run with: Invoke-Pester ./tests/ILVerify/ilverify.Tests.ps1
4+
5+
# Extract and load only the function definitions from ilverify.ps1
6+
# without executing the build/verification logic.
7+
$scriptContent = Get-Content "$PSScriptRoot/ilverify.ps1" -Raw
8+
$ast = [System.Management.Automation.Language.Parser]::ParseInput($scriptContent, [ref]$null, [ref]$null)
9+
$functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false)
10+
foreach ($fn in $functions) {
11+
Invoke-Expression $fn.Extent.Text
12+
}
13+
14+
Describe "Normalize-IlverifyOutputLine" {
15+
It "removes closure suffixes" {
16+
Normalize-IlverifyOutputLine 'Foo+clo@924-516::Invoke()' | Should Be 'Foo+clo::Invoke()'
17+
}
18+
19+
It "removes function suffixes with line numbers" {
20+
Normalize-IlverifyOutputLine 'parseOption@269::Bar()' | Should Be 'parseOption::Bar()'
21+
}
22+
23+
It "removes 'at line NNNN'" {
24+
Normalize-IlverifyOutputLine 'something at line 1234 rest' | Should Be 'something rest'
25+
}
26+
27+
It "removes pipe stage patterns" {
28+
Normalize-IlverifyOutputLine 'Foo+Pipe #1 stage #1 at line 1782@1782::Invoke()' | Should Be 'Foo+::Invoke()'
29+
}
30+
31+
It "collapses multiple spaces" {
32+
Normalize-IlverifyOutputLine 'a b c' | Should Be 'a b c'
33+
}
34+
35+
It "trims leading and trailing whitespace" {
36+
Normalize-IlverifyOutputLine ' hello world ' | Should Be 'hello world'
37+
}
38+
}
39+
40+
Describe "Remove-IlverifyOffsets" {
41+
It "strips a single offset from an error line" {
42+
$line = "[IL]: Error [StackByRef]: : Foo::Bar(int32)][offset 0x0000001E][found Native Int] Expected ByRef."
43+
$result = Remove-IlverifyOffsets @($line)
44+
$result | Should Be "[IL]: Error [StackByRef]: : Foo::Bar(int32)][found Native Int] Expected ByRef."
45+
}
46+
47+
It "handles uppercase and lowercase hex digits" {
48+
$result = Remove-IlverifyOffsets @("prefix [offset 0xABcd0012] suffix")
49+
$result | Should Be "prefix suffix"
50+
}
51+
52+
It "trims trailing whitespace" {
53+
$result = Remove-IlverifyOffsets @("some text ")
54+
$result | Should Be "some text"
55+
}
56+
57+
It "returns empty array for empty input" {
58+
$result = @(Remove-IlverifyOffsets @())
59+
$result.Count | Should Be 0
60+
}
61+
62+
It "processes multiple lines" {
63+
$lines = @(
64+
"[IL]: Error [X]: : A::M()][offset 0x00000011][found Y] Msg1.",
65+
"[IL]: Error [X]: : B::N()][offset 0x00000022][found Z] Msg2."
66+
)
67+
$result = Remove-IlverifyOffsets $lines
68+
$result.Count | Should Be 2
69+
$result[0] | Should Be "[IL]: Error [X]: : A::M()][found Y] Msg1."
70+
$result[1] | Should Be "[IL]: Error [X]: : B::N()][found Z] Msg2."
71+
}
72+
73+
It "passes through lines without offsets unchanged" {
74+
$result = Remove-IlverifyOffsets @("no offsets here")
75+
$result | Should Be "no offsets here"
76+
}
77+
}
78+
79+
Describe "Soft comparison (offset-tolerant)" {
80+
It "matches when only IL offsets differ" {
81+
$output = @(
82+
"[IL]: Error [StackByRef]: : Foo::Bar()][offset 0x0000001E][found Native Int] Expected ByRef.",
83+
"[IL]: Error [ReturnPtrToStack]: : Baz::Qux()][offset 0x00000070] Return type is ByRef."
84+
)
85+
$baseline = @(
86+
"[IL]: Error [StackByRef]: : Foo::Bar()][offset 0x0000001A][found Native Int] Expected ByRef.",
87+
"[IL]: Error [ReturnPtrToStack]: : Baz::Qux()][offset 0x00000064] Return type is ByRef."
88+
)
89+
$cmp = Compare-Object (Remove-IlverifyOffsets $output) (Remove-IlverifyOffsets $baseline)
90+
$cmp | Should BeNullOrEmpty
91+
}
92+
93+
It "detects real differences even when offsets also differ" {
94+
$output = @(
95+
"[IL]: Error [StackByRef]: : Foo::Bar()][offset 0x0000001E][found Native Int] Expected ByRef.",
96+
"[IL]: Error [NEW_ERROR]: : New::Method()][offset 0x00000099][found X] New error."
97+
)
98+
$baseline = @(
99+
"[IL]: Error [StackByRef]: : Foo::Bar()][offset 0x0000001A][found Native Int] Expected ByRef.",
100+
"[IL]: Error [ReturnPtrToStack]: : Baz::Qux()][offset 0x00000064] Return type is ByRef."
101+
)
102+
$cmp = Compare-Object (Remove-IlverifyOffsets $output) (Remove-IlverifyOffsets $baseline)
103+
$cmp | Should Not BeNullOrEmpty
104+
}
105+
106+
It "detects added errors" {
107+
$output = @(
108+
"[IL]: Error [X]: : Foo::A()][offset 0x00000011] Msg.",
109+
"[IL]: Error [X]: : Foo::B()][offset 0x00000022] Msg.",
110+
"[IL]: Error [X]: : Foo::C()][offset 0x00000033] New."
111+
)
112+
$baseline = @(
113+
"[IL]: Error [X]: : Foo::A()][offset 0x00000011] Msg.",
114+
"[IL]: Error [X]: : Foo::B()][offset 0x00000022] Msg."
115+
)
116+
$cmp = Compare-Object (Remove-IlverifyOffsets $output) (Remove-IlverifyOffsets $baseline)
117+
$cmp | Should Not BeNullOrEmpty
118+
}
119+
120+
It "detects removed errors" {
121+
$output = @(
122+
"[IL]: Error [X]: : Foo::A()][offset 0x00000011] Msg."
123+
)
124+
$baseline = @(
125+
"[IL]: Error [X]: : Foo::A()][offset 0x00000011] Msg.",
126+
"[IL]: Error [X]: : Foo::B()][offset 0x00000022] Msg."
127+
)
128+
$cmp = Compare-Object (Remove-IlverifyOffsets $output) (Remove-IlverifyOffsets $baseline)
129+
$cmp | Should Not BeNullOrEmpty
130+
}
131+
132+
It "handles trailing whitespace differences between output and baseline" {
133+
$output = @("[IL]: Error [X]: : Foo::A()][offset 0x00000011] Msg. ")
134+
$baseline = @("[IL]: Error [X]: : Foo::A()][offset 0x000000FF] Msg.")
135+
$cmp = Compare-Object (Remove-IlverifyOffsets $output) (Remove-IlverifyOffsets $baseline)
136+
$cmp | Should BeNullOrEmpty
137+
}
138+
}

tests/ILVerify/ilverify.ps1

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ function Normalize-IlverifyOutputLine {
1818
return $line
1919
}
2020

21+
function Remove-IlverifyOffsets {
22+
param(
23+
[string[]]$lines
24+
)
25+
# Strip IL byte offsets and trailing whitespace for soft comparison.
26+
# Offsets like [offset 0x0000001E] change when code above is modified,
27+
# even though the verification error itself is the same.
28+
return @($lines | ForEach-Object {
29+
($_ -replace '\[offset 0x[0-9A-Fa-f]+\]', '').Trim()
30+
})
31+
}
32+
2133
# Set build script based on which OS we're running on - Windows (build.cmd), Linux or macOS (build.sh)
2234

2335
Write-Host "Checking whether running on Windows: $IsWindows"
@@ -37,8 +49,8 @@ $env:PublishWindowsPdb = "false"
3749
# Set configurations to build
3850
[string[]] $configurations = @("Debug", "Release")
3951

40-
# The following are not passing ilverify checks, so we ignore them for now
41-
[string[]] $ignore_errors = @() # @("StackUnexpected", "UnmanagedPointer", "StackByRef", "ReturnPtrToStack", "ExpectedNumericType", "StackUnderflow")
52+
# Error types that should be excluded from verification via the -g flag (currently none).
53+
[string[]] $ignore_errors = @()
4254

4355
[string] $default_tfm = "netstandard2.0"
4456
# Read product TFM from centralized source of truth via MSBuild
@@ -197,19 +209,31 @@ foreach ($project in $projects.Keys) {
197209
if (-not $cmp) {
198210
Write-Host "ILverify output matches baseline."
199211
} else {
200-
Write-Host "ILverify output does not match baseline, differences:"
212+
# Exact match failed — try soft comparison ignoring IL byte offsets and whitespace.
213+
# IL offsets drift when code above the error site changes, even though the
214+
# verification error itself is semantically identical.
215+
$outputSoft = Remove-IlverifyOffsets $ilverify_output
216+
$baselineSoft = Remove-IlverifyOffsets $baseline
217+
$cmpSoft = Compare-Object $outputSoft $baselineSoft
218+
219+
if (-not $cmpSoft) {
220+
Write-Host "ILverify output matches baseline (IL offsets differ, errors are the same)."
221+
Write-Host " Consider updating baselines: run with TEST_UPDATE_BSL=1 or use '/run ilverify' PR comment."
222+
} else {
223+
Write-Host "ILverify output does not match baseline, differences:"
201224

202-
$cmp | Format-Table -AutoSize -Wrap | Out-String | Write-Host
225+
$cmp | Format-Table -AutoSize -Wrap | Out-String | Write-Host
203226

204-
# Update baselines if TEST_UPDATE_BSL is set to 1
205-
if ($env:TEST_UPDATE_BSL -eq "1") {
206-
Write-Host "Updating baseline file: $baseline_file"
207-
$ilverify_output | Set-Content $baseline_file
208-
} else {
209-
$ilverify_output | Set-Content $baseline_actual_file
227+
# Update baselines if TEST_UPDATE_BSL is set to 1
228+
if ($env:TEST_UPDATE_BSL -eq "1") {
229+
Write-Host "Updating baseline file: $baseline_file"
230+
$ilverify_output | Set-Content $baseline_file
231+
} else {
232+
$ilverify_output | Set-Content $baseline_actual_file
233+
}
234+
$failed = $true
235+
continue
210236
}
211-
$failed = $true
212-
continue
213237
}
214238

215239

0 commit comments

Comments
 (0)