Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8f87f7a
Add feature design document for #elif preprocessor directive
T-Gro Feb 13, 2026
b8a0984
Implement #elif preprocessor directive (F# 11.0)
T-Gro Feb 13, 2026
4edb766
Fix missing language version check for #elif in nested-inactive ifdef…
T-Gro Feb 16, 2026
75ca0e9
Add test for nested #elif langversion check (n > 0 path)
T-Gro Feb 16, 2026
92f183f
Add missing #elif compiler directive tests
T-Gro Feb 16, 2026
22e64ec
Add missing #elif preprocessor directive tests (T1-T3)
T-Gro Feb 16, 2026
0528a4b
Add missing compiler directive tests (T1-T3)
T-Gro Feb 16, 2026
0cfc903
Fix elifMustBeFirstWarning test to actually verify FS3882 for #elif
T-Gro Feb 16, 2026
90a32a1
Add IDE/tooling tests for #elif directive (T4-T7)
T-Gro Feb 16, 2026
e1bb328
Refactor: reduce duplication in #elif preprocessor handling
T-Gro Feb 16, 2026
14fec3d
Merge branch 'main' into feature-directive-elif
T-Gro Feb 17, 2026
b101973
Add tests for IfDefElif stack state transitions
T-Gro Feb 17, 2026
75f9225
Assert error codes FS3882/FS3883 in #elif diagnostic tests
T-Gro Feb 17, 2026
833b476
Add #elif signature file syntax tree baseline
T-Gro Feb 17, 2026
1708c02
Add FSharpLexer.Tokenize test for HashElif token kind
T-Gro Feb 17, 2026
e9aea76
Refactor lex.fsl: remove redundant bindings, improve comments
T-Gro Feb 17, 2026
6a345f5
Refactor LexerStore: extract saveSimpleHash for Else/EndIf
T-Gro Feb 17, 2026
7f18485
Fix #elif docs: forward compat caveat, update trivia comment
T-Gro Feb 17, 2026
6604fc0
Add release notes for #elif preprocessor directive
T-Gro Feb 17, 2026
14f97fa
Add VS Editor colorization tests for #elif keyword and grayout
T-Gro Feb 17, 2026
1a2f64f
Merge branch 'main' into feature-directive-elif
T-Gro Feb 18, 2026
d139b1f
Fix FSComp.txt error code sorting and update ILVerify baselines
T-Gro Feb 18, 2026
0395fc6
Merge branch 'main' into feature-directive-elif
T-Gro Feb 18, 2026
34eeaa3
Fix ILVerify baselines: add entries to netstandard2.0, revert net10.0
T-Gro Feb 18, 2026
5b00343
Fix ILVerify baselines: correct ServiceLexing entries for all 4 configs
T-Gro Feb 18, 2026
fade9f3
Fix ILVerify net10.0 baselines: remove shifted original offsets
T-Gro Feb 18, 2026
20a16b5
Merge branch 'main' into feature-directive-elif
T-Gro Feb 19, 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
36 changes: 36 additions & 0 deletions claims-coverage.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should delete this one

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Claims Coverage Report for PR #19297

## Findings

### Implemented Claims
1. **Fixes #19296 - State machines: low-level resumable code not always expanded correctly**
- **Location**: `src/Compiler/Optimize/LowerStateMachines.fs:178` (`BindResumableCodeDefinitions`), `tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs:57`
- **Description**: Implemented logic to eliminate `IfUseResumableStateMachinesExpr` by taking the static branch and recursively binding resumable code definitions, including looking through debug points. Verified by `Nested __useResumableCode is expanded correctly` test case.
- **Severity**: **None (Verified)**

2. **Fixes #12839 - Unexpected FS3511 warning for big records in tasks**
- **Location**: `src/Compiler/Optimize/LowerStateMachines.fs:242` (`TryReduceApp`), `tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs:274`
- **Description**: Enhanced `TryReduceApp` to better track and resolve resumable code variables and push applications through, enabling static compilation for complex constructs (like big records and nested matches) that previously fell back to dynamic code (triggering FS3511). Verified by `Big record` test case.
- **Severity**: **None (Verified)**

3. **Includes test cases from #14930**
- **Location**: `tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs:225`
- **Description**: Added `Task with for loop over tuples compiles statically` test case, which was the repro for #14930. The code changes in `LowerStateMachines.fs` support this scenario.
- **Severity**: **None (Verified)**

4. **Issue #13404 - Referenced in NestedTaskFailures.fs**
- **Location**: `tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/NestedTaskFailures.fs`
- **Description**: Removed `#nowarn "3511"` from the file, indicating that the nested task failures are now statically compiled correctly and don't trigger the warning/error.
- **Severity**: **None (Verified)**

### Orphan Claims
- None found. All claims in the PR description and linked issues have corresponding code changes and tests.

### Partial Implementations
- None found. The implementations appear to fully address the described issues based on the provided repro cases.

### Orphan Changes
- None found. All code changes in `LowerStateMachines.fs` are directly related to improving the static compilation of state machines as described in the PR.

## Summary
The PR covers all its claims with appropriate code changes and regression tests. The changes in `LowerStateMachines.fs` logically address the issues of incorrect expansion and fallback to dynamic code. The added tests cover the reported reproduction scenarios.
144 changes: 144 additions & 0 deletions docs/feature-elif-preprocessor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# `#elif` Preprocessor Directive
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is more of a reviewer pack, will delete it before merging.


- Language suggestion: [fslang-suggestions#1370](https://github.com/fsharp/fslang-suggestions/issues/1370) (approved-in-principle)

F# adds the `#elif` preprocessor directive for conditional compilation, aligning with C# and reducing nesting depth when checking multiple conditions.

## Motivation

Currently F# only has `#if`, `#else`, and `#endif` for conditional compilation. When you need to check multiple conditions, you must nest `#if` inside `#else` blocks, leading to deeply indented code:

```fsharp
let myPath =
#if WIN64
"/library/x64/runtime.dll"
#else
#if WIN86
"/library/x86/runtime.dll"
#else
#if MAC
"/library/iOS/runtime-osx.dll"
#else
"/library/unix/runtime.dll"
#endif
#endif
#endif
```

An alternative workaround uses repeated `#if` blocks with complex negated conditions, which is error-prone and verbose:

```fsharp
let myPath =
#if WIN64
"/library/x64/runtime.dll"
#endif
#if WIN86
"/library/x86/runtime.dll"
#endif
#if MAC
"/library/iOS/runtime-osx.dll"
#endif
#if !WIN64 && !WIN86 && !MAC
"/library/unix/runtime.dll"
#endif
```

Both approaches are harder to read, maintain, and extend than they need to be.

## Feature Description

With `#elif`, the same logic is flat and clear:

```fsharp
let myPath =
#if WIN64
"/library/x64/runtime.dll"
#elif WIN86
"/library/x86/runtime.dll"
#elif MAC
"/library/iOS/runtime-osx.dll"
#else
"/library/unix/runtime.dll"
#endif
```

### Semantics

- `#elif` is short for "else if" in the preprocessor.
- It evaluates its condition only if no previous `#if` or `#elif` branch in the same chain was active.
- Only one branch in a `#if`/`#elif`/`#else`/`#endif` chain is ever active.
- `#elif` supports the same boolean expressions as `#if`: identifiers, `&&`, `||`, `!`, and parentheses.
- `#elif` can appear zero or more times between `#if` and `#else`/`#endif`.
- `#elif` after `#else` is an error.
- `#elif` without a matching `#if` is an error.
- `#elif` blocks can be nested inside other `#if`/`#elif`/`#else` blocks.
- Each `#elif` must appear at the start of a line (same rule as `#if`/`#else`/`#endif`).

## Detailed Semantics

The following table shows which branch is active for a `#if A` / `#elif B` / `#else` chain under all combinations of A and B:

| Source | A=true, B=true | A=true, B=false | A=false, B=true | A=false, B=false |
|---|---|---|---|---|
| `#if A` block | **active** | **active** | skip | skip |
| `#elif B` block | skip | skip | **active** | skip |
| `#else` block | skip | skip | skip | **active** |

Only the **first** matching branch is active. When both A and B are true, the `#if A` block is active and the `#elif B` block is skipped — the `#elif` condition is never evaluated.

## Language Version

- This feature requires F# 11.0 (language version `11.0` or `preview`).
- Using `#elif` with an older language version produces a compiler error directing the user to upgrade.

## F# Language Specification Changes

This feature requires changes to [section 3.3 Conditional Compilation](https://fsharp.github.io/fslang-spec/lexical-analysis/#33-conditional-compilation) of the F# Language Specification.

### Grammar

**Current spec grammar:**

```
token if-directive = "#if" whitespace if-expression-text
token else-directive = "#else"
token endif-directive = "#endif"
```

**Proposed spec grammar (add `elif-directive`):**

```
token if-directive = "#if" whitespace if-expression-text
token elif-directive = "#elif" whitespace if-expression-text
token else-directive = "#else"
token endif-directive = "#endif"
```

### Description Updates

The current spec says:

> If an `if-directive` token is matched during tokenization, text is recursively tokenized until a corresponding `else-directive` or `endif-directive`.

This should be updated to:

> If an `if-directive` token is matched during tokenization, text is recursively tokenized until a corresponding `elif-directive`, `else-directive`, or `endif-directive`. An `elif-directive` evaluates its condition only if no preceding `if-directive` or `elif-directive` in the same chain evaluated to true. At most one branch in an `if`/`elif`/`else` chain is active.

Additionally:

> An `elif-directive` must appear after an `if-directive` or another `elif-directive`, and before any `else-directive`. An `elif-directive` after an `else-directive` is an error.

## Tooling Impact

This change affects:

- **Fantomas** (F# code formatter) — will need to recognize `#elif` for formatting.
- **FSharp.Compiler.Service consumers** — new `ConditionalDirectiveTrivia.Elif` case, new `FSharpTokenKind.HashElif`.
- **Any tool** that processes F# source code with preprocessor directives.

These are non-breaking changes since `#elif` was previously a syntax error, so no existing valid F# code uses it.

## Compatibility

- **Backward compatible**: Old code without `#elif` continues to work unchanged.
- **Forward compatible**: Code using `#elif` will produce a clear error on older compilers when `#elif` appears in active code. However, if `#elif` appears inside an inactive `#if` branch (e.g., `#if FALSE` / `#elif X` / `#endif`), older compilers silently skip the `#elif` line as inactive text without error, potentially producing wrong branch selection. The language version gate (`LanguageFeature.PreprocessorElif` at F# 11.0) prevents this scenario in practice by requiring a compiler that understands `#elif`.
1 change: 1 addition & 0 deletions docs/release-notes/.Language/11.0.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
### Added

* Simplify implementation of interface hierarchies with equally named abstract slots: when a derived interface provides a Default Interface Member (DIM) implementation for a base interface slot, F# no longer requires explicit interface declarations for the DIM-covered slot. ([Language suggestion #1430](https://github.com/fsharp/fslang-suggestions/issues/1430), [RFC FS-1336](https://github.com/fsharp/fslang-design/pull/826), [PR #19241](https://github.com/dotnet/fsharp/pull/19241))
* Support `#elif` preprocessor directive ([Language suggestion #1370](https://github.com/fsharp/fslang-suggestions/issues/1370), [RFC FS-1334](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1334-elif-preprocessor-directive.md), [PR #XXXXX](https://github.com/dotnet/fsharp/pull/XXXXX))

### Fixed

Expand Down
3 changes: 2 additions & 1 deletion src/Compiler/Driver/CompilerDiagnostics.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1284,7 +1284,8 @@ type Exception with
| Parser.TOKEN_HASH_LINE
| Parser.TOKEN_HASH_IF
| Parser.TOKEN_HASH_ELSE
| Parser.TOKEN_HASH_ENDIF -> SR.GetString("Parser.TOKEN.HASH.ENDIF")
| Parser.TOKEN_HASH_ENDIF
| Parser.TOKEN_HASH_ELIF -> SR.GetString("Parser.TOKEN.HASH.ENDIF")
| Parser.TOKEN_INACTIVECODE -> SR.GetString("Parser.TOKEN.INACTIVECODE")
| Parser.TOKEN_LEX_FAILURE -> SR.GetString("Parser.TOKEN.LEX.FAILURE")
| Parser.TOKEN_WHITESPACE -> SR.GetString("Parser.TOKEN.WHITESPACE")
Expand Down
7 changes: 6 additions & 1 deletion src/Compiler/FSComp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,8 @@ lexHashEndingNoMatchingIf,"#endif has no matching #if"
1169,lexHashIfMustHaveIdent,"#if directive should be immediately followed by an identifier"
1170,lexWrongNestedHashEndif,"Syntax error. Wrong nested #endif, unexpected tokens before it."
lexHashBangMustBeFirstInFile,"#! may only appear as the first line at the start of a file."
lexHashElifNoMatchingIf,"#elif has no matching #if"
lexHashElifAfterElse,"#elif is not allowed after #else"
1171,pplexExpectedSingleLineComment,"Expected single line comment or end of line"
1172,memberOperatorDefinitionWithNoArguments,"Infix operator member '%s' has no arguments. Expected a tuple of 2 arguments, e.g. static member (+) (x,y) = ..."
1173,memberOperatorDefinitionWithNonPairArgument,"Infix operator member '%s' has %d initial argument(s). Expected a tuple of 2 arguments, e.g. static member (+) (x,y) = ..."
Expand Down Expand Up @@ -1804,5 +1806,8 @@ featureAllowLetOrUseBangTypeAnnotationWithoutParens,"Allow let! and use! type an
3879,xmlDocNotFirstOnLine,"XML documentation comments should be the first non-whitespace text on a line."
featureReturnFromFinal,"Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder."
featureImplicitDIMCoverage,"Implicit dispatch slot coverage for default interface member implementations"
featurePreprocessorElif,"#elif preprocessor directive"
3880,optsLangVersionOutOfSupport,"Language version '%s' is out of support. The last .NET SDK supporting it is available at https://dotnet.microsoft.com/en-us/download/dotnet/%s"
3881,optsUnrecognizedLanguageFeature,"Unrecognized language feature name: '%s'. Use a valid feature name such as 'NameOf' or 'StringInterpolation'."
3881,optsUnrecognizedLanguageFeature,"Unrecognized language feature name: '%s'. Use a valid feature name such as 'NameOf' or 'StringInterpolation'."
3882,lexHashElifMustBeFirst,"#elif directive must appear as the first non-whitespace character on a line"
3883,lexHashElifMustHaveIdent,"#elif directive should be immediately followed by an identifier"
3 changes: 3 additions & 0 deletions src/Compiler/Facilities/LanguageFeatures.fs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ type LanguageFeature =
| AllowTypedLetUseAndBang
| ReturnFromFinal
| ImplicitDIMCoverage
| PreprocessorElif

/// LanguageVersion management
type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) =
Expand Down Expand Up @@ -246,6 +247,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array)

// F# 11.0
// Put stabilized features here for F# 11.0 previews via .NET SDK preview channels
LanguageFeature.PreprocessorElif, languageVersion110

// Difference between languageVersion110 and preview - 11.0 gets turned on automatically by picking a preview .NET 11 SDK
// previewVersion is only when "preview" is specified explicitly in project files and users also need a preview SDK
Expand Down Expand Up @@ -443,6 +445,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array)
| LanguageFeature.AllowTypedLetUseAndBang -> FSComp.SR.featureAllowLetOrUseBangTypeAnnotationWithoutParens ()
| LanguageFeature.ReturnFromFinal -> FSComp.SR.featureReturnFromFinal ()
| LanguageFeature.ImplicitDIMCoverage -> FSComp.SR.featureImplicitDIMCoverage ()
| LanguageFeature.PreprocessorElif -> FSComp.SR.featurePreprocessorElif ()

/// Get a version string associated with the given feature.
static member GetFeatureVersionString feature =
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Facilities/LanguageFeatures.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ type LanguageFeature =
| AllowTypedLetUseAndBang
| ReturnFromFinal
| ImplicitDIMCoverage
| PreprocessorElif

/// LanguageVersion management
type LanguageVersion =
Expand Down
32 changes: 22 additions & 10 deletions src/Compiler/Service/ServiceLexing.fs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,8 @@ module internal TokenClassifications =
| WARN_DIRECTIVE _
| HASH_IF _
| HASH_ELSE _
| HASH_ENDIF _ -> (FSharpTokenColorKind.PreprocessorKeyword, FSharpTokenCharKind.WhiteSpace, FSharpTokenTriggerClass.None)
| HASH_ENDIF _
| HASH_ELIF _ -> (FSharpTokenColorKind.PreprocessorKeyword, FSharpTokenCharKind.WhiteSpace, FSharpTokenTriggerClass.None)

| INACTIVECODE _ -> (FSharpTokenColorKind.InactiveCode, FSharpTokenCharKind.WhiteSpace, FSharpTokenTriggerClass.None)

Expand Down Expand Up @@ -483,6 +484,7 @@ module internal LexerStateEncoding =
| HASH_IF(_, _, cont)
| HASH_ELSE(_, _, cont)
| HASH_ENDIF(_, _, cont)
| HASH_ELIF(_, _, cont)
| INACTIVECODE cont
| WHITESPACE cont
| COMMENT cont
Expand All @@ -505,7 +507,7 @@ module internal LexerStateEncoding =
let lexstateNumBits = 4
let ncommentsNumBits = 4
let ifdefstackCountNumBits = 8
let ifdefstackNumBits = 24 // 0 means if, 1 means else
let ifdefstackNumBits = 24 // 2 bits per entry: 00=if, 01=else, 10=elif
let stringKindBits = 3
let nestingBits = 12
let delimLenBits = 3
Expand Down Expand Up @@ -587,8 +589,9 @@ module internal LexerStateEncoding =

for ifOrElse in ifdefStack do
match ifOrElse with
| IfDefIf, _ -> ()
| IfDefElse, _ -> ifdefStackBits <- (ifdefStackBits ||| (1 <<< ifdefStackCount))
| IfDefIf, _ -> () // 0b00, already zero
| IfDefElse, _ -> ifdefStackBits <- (ifdefStackBits ||| (0b01 <<< (ifdefStackCount * 2)))
| IfDefElif, _ -> ifdefStackBits <- (ifdefStackBits ||| (0b10 <<< (ifdefStackCount * 2)))

ifdefStackCount <- ifdefStackCount + 1

Expand Down Expand Up @@ -646,9 +649,15 @@ module internal LexerStateEncoding =
let ifdefStack = int32 ((bits &&& ifdefstackMask) >>> ifdefstackStart)

for i in 1..ifdefStackCount do
let bit = ifdefStackCount - i
let mask = 1 <<< bit
let ifDef = (if ifdefStack &&& mask = 0 then IfDefIf else IfDefElse)
let bitPos = (ifdefStackCount - i) * 2
let value = (ifdefStack >>> bitPos) &&& 0b11

let ifDef =
match value with
| 0b01 -> IfDefElse
| 0b10 -> IfDefElif
| _ -> IfDefIf

ifDefs <- (ifDef, range0) :: ifDefs

let stringKindValue = int32 ((bits &&& stringKindMask) >>> stringKindStart)
Expand Down Expand Up @@ -821,12 +830,12 @@ type FSharpLineTokenizer(lexbuf: UnicodeLexing.Lexbuf, maxLength: int option, fi

// Split the following line:
// anywhite* "#if" anywhite+ ident anywhite* ("//" [^'\n''\r']*)?
let processHashIfLine ofs (str: string) cont =
let processHashIfLine ofs (str: string) directiveLen cont =
let With n m = if (n < 0) then m else n

processDirectiveLine ofs (fun delay ->
// Process: anywhite* "#if"
let offset = processDirective str 2 delay cont
let offset = processDirective str directiveLen delay cont
// Process: anywhite+ ident
let rest, spaces =
let w = str.Substring offset
Expand Down Expand Up @@ -944,7 +953,8 @@ type FSharpLineTokenizer(lexbuf: UnicodeLexing.Lexbuf, maxLength: int option, fi
// because sometimes token shouldn't be split. However it is just for colorization &
// for VS (which needs to recognize when user types ".").
match token with
| HASH_IF(m, lineStr, cont) when lineStr <> "" -> false, processHashIfLine m.StartColumn lineStr cont
| HASH_IF(m, lineStr, cont) when lineStr <> "" -> false, processHashIfLine m.StartColumn lineStr 2 cont
| HASH_ELIF(m, lineStr, cont) when lineStr <> "" -> false, processHashIfLine m.StartColumn lineStr 4 cont
| HASH_ELSE(m, lineStr, cont) when lineStr <> "" -> false, processHashEndElse m.StartColumn lineStr 4 cont
| HASH_ENDIF(m, lineStr, cont) when lineStr <> "" -> false, processHashEndElse m.StartColumn lineStr 5 cont
| WARN_DIRECTIVE(_, s, cont) -> false, processWarnDirective s leftc rightc cont
Expand Down Expand Up @@ -1180,6 +1190,7 @@ type FSharpTokenKind =
| HashIf
| HashElse
| HashEndIf
| HashElif
| WarnDirective
| CommentTrivia
| WhitespaceTrivia
Expand Down Expand Up @@ -1379,6 +1390,7 @@ type FSharpToken =
| HASH_IF _ -> FSharpTokenKind.HashIf
| HASH_ELSE _ -> FSharpTokenKind.HashElse
| HASH_ENDIF _ -> FSharpTokenKind.HashEndIf
| HASH_ELIF _ -> FSharpTokenKind.HashElif
| WARN_DIRECTIVE _ -> FSharpTokenKind.WarnDirective
| COMMENT _ -> FSharpTokenKind.CommentTrivia
| WHITESPACE _ -> FSharpTokenKind.WhitespaceTrivia
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Service/ServiceLexing.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ type public FSharpTokenKind =
| HashIf
| HashElse
| HashEndIf
| HashElif
| WarnDirective
| CommentTrivia
| WhitespaceTrivia
Expand Down
Loading
Loading