Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0d59867
Initial plan
Copilot Jan 5, 2026
cff9c64
Implement StringLengthZero optimization for empty string patterns
Copilot Jan 5, 2026
2e05600
Add IL test for empty string pattern optimization
Copilot Jan 5, 2026
5955499
Add release notes for empty string pattern optimization
Copilot Jan 5, 2026
06fcf40
Fix regression: revert StringLengthZero discriminator, keep optimizat…
Copilot Jan 6, 2026
60a9614
Add inlining test for empty string pattern optimization with pickle c…
Copilot Jan 6, 2026
64ae956
Remove all StringLengthZero usages, update tests with bundled clauses…
Copilot Jan 6, 2026
c4b35b0
WIP: Add isNullFiltered parameter to BuildSwitch to track null-safe c…
Copilot Jan 6, 2026
09553a6
Remove debug output from BuildSwitch
Copilot Jan 7, 2026
0d88da5
Merge branch 'main' into copilot/optimize-empty-string-check
T-Gro Jan 8, 2026
52d4206
Add debugging instrumentation to BuildSwitch for isNullFiltered inves…
Copilot Jan 8, 2026
68bc4c1
Add check for IsNull edge in foldBack to attempt eliminating redundan…
Copilot Jan 9, 2026
cdbd93d
Simplify isNullFiltered logic - remove hasIsNullEdge computation
Copilot Jan 9, 2026
50dd80c
Merge branch 'main' into copilot/optimize-empty-string-check
T-Gro Jan 13, 2026
b3fad47
Apply patch from /run ilverify
actions-user Jan 13, 2026
37db342
Remove all debug instrumentation and verbose flag
Copilot Jan 20, 2026
f693c05
Merge branch 'main' into copilot/optimize-empty-string-check
T-Gro Jan 22, 2026
bdea174
Merge branch 'main' into copilot/optimize-empty-string-check
T-Gro Jan 22, 2026
3328d9c
Merge branch 'main' into copilot/optimize-empty-string-check
T-Gro Jan 22, 2026
a86e142
Apply patch from /run ilverify
actions-user Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/10.0.200.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Type relations cache: optimize key generation ([Issue #19116](https://github.com/dotnet/fsharp/issues/18767)) ([PR #19120](https://github.com/dotnet/fsharp/pull/19120))
* Fixed QuickParse to correctly handle optional parameter syntax with `?` prefix, resolving syntax highlighting issues. ([Issue #11008753](https://developercommunity.visualstudio.com/t/F-Highlighting-fails-on-optional-parame/11008753)) ([PR #XXXXX](https://github.com/dotnet/fsharp/pull/XXXXX))
* Fix `--preferreduilang` switch leaking into `fsi.CommandLineArgs` when positioned after script file ([PR #19151](https://github.com/dotnet/fsharp/pull/19151))
* Optimize empty string pattern matching to use null-safe .Length check instead of string equality comparison for better performance.
* Fixed runtime crash when using interfaces with unimplemented static abstract members as constrained type arguments. ([Issue #19184](https://github.com/dotnet/fsharp/issues/19184))
* Fix delegates with `[<OptionalArgument>]` and caller info attributes failing to compile. ([Issue #18868](https://github.com/dotnet/fsharp/issues/18868), [PR #19069](https://github.com/dotnet/fsharp/pull/19069))
* Type checker: mark generated event tree nodes as synthetic ([PR #19213](https://github.com/dotnet/fsharp/pull/19213))
Expand Down
23 changes: 17 additions & 6 deletions src/Compiler/Checking/PatternMatchCompilation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -767,8 +767,7 @@ let (|ConstNeedsDefaultCase|_|) c =
/// - Compact integer switches become a single switch. Non-compact integer
/// switches, string switches and floating point switches are treated in the
/// same way as DecisionTreeTest.IsInst.
let rec BuildSwitch inpExprOpt g expr edges dflt m =
if verbose then dprintf "--> BuildSwitch@%a, #edges = %A, dflt.IsSome = %A\n" outputRange m (List.length edges) (Option.isSome dflt)
let rec BuildSwitch inpExprOpt g isNullFiltered expr edges dflt m =
match edges, dflt with
| [], None -> failwith "internal error: no edges and no default"
| [], Some dflt -> dflt
Expand All @@ -780,11 +779,13 @@ let rec BuildSwitch inpExprOpt g expr edges dflt m =
// In this case the 'expr' already holds the result of the 'isinst' test.

| TCase(DecisionTreeTest.IsInst _, success) :: edges, dflt when Option.isSome inpExprOpt ->
TDSwitch(expr, [TCase(DecisionTreeTest.IsNull, BuildSwitch None g expr edges dflt m)], Some success, m)
TDSwitch(expr, [TCase(DecisionTreeTest.IsNull, BuildSwitch None g false expr edges dflt m)], Some success, m)

// isnull and isinst tests
| TCase((DecisionTreeTest.IsNull | DecisionTreeTest.IsInst _), _) as edge :: edges, dflt ->
TDSwitch(expr, [edge], Some (BuildSwitch None g expr edges dflt m), m)
// After an IsNull test, in the fallthrough branch (Some), we know the value is not null
let nullFiltered = match edge with TCase(DecisionTreeTest.IsNull, _) -> true | _ -> isNullFiltered
TDSwitch(expr, [edge], Some (BuildSwitch None g nullFiltered expr edges dflt m), m)

// All these should also always have default cases
| TCase(DecisionTreeTest.Const ConstNeedsDefaultCase, _) :: _, None ->
Expand All @@ -799,7 +800,17 @@ let rec BuildSwitch inpExprOpt g expr edges dflt m =
match discrim with
| DecisionTreeTest.ArrayLength(n, _) ->
let _v, vExpr, bind = mkCompGenLocalAndInvisibleBind g "testExpr" m testexpr
mkLetBind m bind (mkLazyAnd g m (mkNonNullTest g m vExpr) (mkILAsmCeq g m (mkLdlen g m vExpr) (mkInt g m n)))
// Skip null check if we're in a null-filtered context
let test = mkILAsmCeq g m (mkLdlen g m vExpr) (mkInt g m n)
let finalTest = if isNullFiltered then test else mkLazyAnd g m (mkNonNullTest g m vExpr) test
mkLetBind m bind finalTest
| DecisionTreeTest.Const (Const.String "") ->
// Optimize empty string check to use null-safe length check
let _v, vExpr, bind = mkCompGenLocalAndInvisibleBind g "testExpr" m testexpr
let test = mkILAsmCeq g m (mkGetStringLength g m vExpr) (mkInt g m 0)
// Skip null check if we're in a null-filtered context
let finalTest = if isNullFiltered then test else mkLazyAnd g m (mkNonNullTest g m vExpr) test
mkLetBind m bind finalTest
| DecisionTreeTest.Const (Const.String _ as c) ->
mkCallEqualsOperator g m g.string_ty testexpr (Expr.Const (c, m, g.string_ty))
| DecisionTreeTest.Const (Const.Decimal _ as c) ->
Expand Down Expand Up @@ -1152,7 +1163,7 @@ let CompilePatternBasic
// OK, build the whole tree and whack on the binding if any
let finalDecisionTree =
let inpExprToSwitch = (match inpExprOpt with Some vExpr -> vExpr | None -> GetSubExprOfInput subexpr)
let tree = BuildSwitch inpExprOpt g inpExprToSwitch simulSetOfCases defaultTreeOpt mMatch
let tree = BuildSwitch inpExprOpt g false inpExprToSwitch simulSetOfCases defaultTreeOpt mMatch
match bindOpt with
| None -> tree
| Some bind -> TDBind (bind, tree)
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/TypedTree/TypedTreeOps.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -2348,6 +2348,8 @@ val mkIncr: TcGlobals -> range -> Expr -> Expr

val mkLdlen: TcGlobals -> range -> Expr -> Expr

val mkGetStringLength: TcGlobals -> range -> Expr -> Expr

val mkLdelem: TcGlobals -> range -> TType -> Expr -> Expr -> Expr

//-------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module TestLibrary

let inline classifyString (s: string) =
match s with
| "" -> "empty"
| null -> "null"
| _ -> "other"

let inline testEmptyStringOnly (s: string) =
match s with
| "" -> 1
| _ -> 0

let inline testBundledNullAndEmpty (s: string) =
match s with
| null | "" -> 0
| _ -> 1

let inline testBundledEmptyAndNull (s: string) =
match s with
| "" | null -> 0
| _ -> 1

// Usage functions to show inlining in action
let useClassifyString s = classifyString s
let useTestEmptyStringOnly s = testEmptyStringOnly s
let useBundledNullAndEmpty s = testBundledNullAndEmpty s
let useBundledEmptyAndNull s = testBundledEmptyAndNull s
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@





.assembly extern runtime { }
.assembly extern FSharp.Core { }
.assembly assembly
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.FSharpInterfaceDataVersionAttribute::.ctor(int32,
int32,
int32) = ( 01 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 )




.hash algorithm 0x00008004
.ver 0:0:0:0
}
.module assembly.exe

.imagebase {value}
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003
.corflags 0x00000001





.class public abstract auto ansi sealed TestLibrary
extends [runtime]System.Object
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 )
.method public static string classifyString(string s) cil managed
{

.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: brfalse.s IL_0010

IL_0004: ldarg.0
IL_0005: callvirt instance int32 [runtime]System.String::get_Length()
IL_000a: ldc.i4.0
IL_000b: ceq
IL_000d: nop
IL_000e: br.s IL_0012

IL_0010: ldc.i4.0
IL_0011: nop
IL_0012: brtrue.s IL_0019

IL_0014: ldarg.0
IL_0015: brfalse.s IL_001f

IL_0017: br.s IL_0025

IL_0019: ldstr "empty"
IL_001e: ret

IL_001f: ldstr "null"
IL_0024: ret

IL_0025: ldstr "other"
IL_002a: ret
}

.method public static int32 testEmptyStringOnly(string s) cil managed
{

.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: brfalse.s IL_000e

IL_0004: ldarg.0
IL_0005: callvirt instance int32 [runtime]System.String::get_Length()
IL_000a: brtrue.s IL_000e

IL_000c: ldc.i4.1
IL_000d: ret

IL_000e: ldc.i4.0
IL_000f: ret
}

.method public static int32 testBundledNullAndEmpty(string s) cil managed
{

.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: brfalse.s IL_0017

IL_0004: ldarg.0
IL_0005: brfalse.s IL_0013

IL_0007: ldarg.0
IL_0008: callvirt instance int32 [runtime]System.String::get_Length()
IL_000d: ldc.i4.0
IL_000e: ceq
IL_0010: nop
IL_0011: br.s IL_0015

IL_0013: ldc.i4.0
IL_0014: nop
IL_0015: brfalse.s IL_0019

IL_0017: ldc.i4.0
IL_0018: ret

IL_0019: ldc.i4.1
IL_001a: ret
}

.method public static int32 testBundledEmptyAndNull(string s) cil managed
{

.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: brfalse.s IL_0010

IL_0004: ldarg.0
IL_0005: callvirt instance int32 [runtime]System.String::get_Length()
IL_000a: ldc.i4.0
IL_000b: ceq
IL_000d: nop
IL_000e: br.s IL_0012

IL_0010: ldc.i4.0
IL_0011: nop
IL_0012: brtrue.s IL_0017

IL_0014: ldarg.0
IL_0015: brtrue.s IL_0019

IL_0017: ldc.i4.0
IL_0018: ret

IL_0019: ldc.i4.1
IL_001a: ret
}

.method public static string useClassifyString(string s) cil managed
{

.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: brfalse.s IL_0010

IL_0004: ldarg.0
IL_0005: callvirt instance int32 [runtime]System.String::get_Length()
IL_000a: ldc.i4.0
IL_000b: ceq
IL_000d: nop
IL_000e: br.s IL_0012

IL_0010: ldc.i4.0
IL_0011: nop
IL_0012: brtrue.s IL_0019

IL_0014: ldarg.0
IL_0015: brfalse.s IL_001f

IL_0017: br.s IL_0025

IL_0019: ldstr "empty"
IL_001e: ret

IL_001f: ldstr "null"
IL_0024: ret

IL_0025: ldstr "other"
IL_002a: ret
}

.method public static int32 useTestEmptyStringOnly(string s) cil managed
{

.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: brfalse.s IL_000e

IL_0004: ldarg.0
IL_0005: callvirt instance int32 [runtime]System.String::get_Length()
IL_000a: brtrue.s IL_000e

IL_000c: ldc.i4.1
IL_000d: ret

IL_000e: ldc.i4.0
IL_000f: ret
}

.method public static int32 useBundledNullAndEmpty(string s) cil managed
{

.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: brfalse.s IL_0017

IL_0004: ldarg.0
IL_0005: brfalse.s IL_0013

IL_0007: ldarg.0
IL_0008: callvirt instance int32 [runtime]System.String::get_Length()
IL_000d: ldc.i4.0
IL_000e: ceq
IL_0010: nop
IL_0011: br.s IL_0015

IL_0013: ldc.i4.0
IL_0014: nop
IL_0015: brfalse.s IL_0019

IL_0017: ldc.i4.0
IL_0018: ret

IL_0019: ldc.i4.1
IL_001a: ret
}

.method public static int32 useBundledEmptyAndNull(string s) cil managed
{

.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: brfalse.s IL_0010

IL_0004: ldarg.0
IL_0005: callvirt instance int32 [runtime]System.String::get_Length()
IL_000a: ldc.i4.0
IL_000b: ceq
IL_000d: nop
IL_000e: br.s IL_0012

IL_0010: ldc.i4.0
IL_0011: nop
IL_0012: brtrue.s IL_0017

IL_0014: ldarg.0
IL_0015: brtrue.s IL_0019

IL_0017: ldc.i4.0
IL_0018: ret

IL_0019: ldc.i4.1
IL_001a: ret
}

}

.class private abstract auto ansi sealed '<StartupCode$assembly>'.$TestLibrary
extends [runtime]System.Object
{
.method public static void main@() cil managed
{
.entrypoint

.maxstack 8
IL_0000: ret
}

}





Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,10 @@ printfn $"{(SecondType ()).SecondMethod()}"
|> compileAndRun
|> shouldSucceed


// Test empty string pattern optimization in inlining
[<Theory; FileInlineData("EmptyStringPattern.fs")>]
let ``EmptyStringPattern_fs`` compilation =
compilation
|> getCompilation
|> verifyCompilation
Loading
Loading