Skip to content

Comments

Feature: #elif preprocessor directive#19323

Open
T-Gro wants to merge 27 commits intomainfrom
feature-directive-elif
Open

Feature: #elif preprocessor directive#19323
T-Gro wants to merge 27 commits intomainfrom
feature-directive-elif

Conversation

@T-Gro
Copy link
Member

@T-Gro T-Gro commented Feb 18, 2026

Inspired by and following up from #19045 by @Thorium.

RFC FS-1334 · Suggestion #1370

What

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

Supports &&, ||, ! in conditions, nesting, and arbitrary chaining. Gated behind --langversion:11.0.

Design decisions

  • New IfDefElif stack entry (not reusing IfDefElse) — lets the lexer distinguish "a branch was taken" from "we are in #else". Uses 2-bit encoding in the IDE ifdef stack, reducing max nesting from 24 to 12 (plenty).
  • Four dedicated diagnostics (lexHashElifNoMatchingIf, lexHashElifAfterElse, lexHashElifMustBeFirst, lexHashElifMustHaveIdent) instead of reusing #else error messages.
  • ConditionalDirectiveTrivia.Elif case added to syntax trivia — enables fold regions, colorization, and grayout in VS without any editor-side changes.

Future work (other repos)

  • Fantomas: teach the formatter about #elif (parse + format roundtrip)
  • FsAutoComplete / Ionide: verify #elif grayout, folding, and completion work end-to-end
  • MS Learn docs: update the conditional compilation page

T-Gro and others added 20 commits February 13, 2026 23:31
Creates docs/feature-elif-preprocessor.md covering motivation, semantics,
spec grammar changes, tooling impact, and compatibility for the #elif
preprocessor directive (fslang-suggestions#1370).
Add #elif support to the F# lexer with IfDefElif stack entry tracking
first-match-wins semantics. The lexer correctly handles:
- Basic #if/#elif/#else/#endif chains
- Multiple #elif directives
- Nested #elif inside other #if blocks
- Error cases (#elif after #else, #elif without #if)
- Language version gating (requires --langversion:11.0)

Fix IDE state serialization to use 2 bits per ifdef stack entry
(00=if, 01=else, 10=elif) so IfDefElif state survives
encodeLexCont/decodeLexCont round-trips correctly.
…Skip path

Add CheckLanguageFeatureAndRecover for PreprocessorElif in the n > 0 branch
of the ifdefSkip rule, matching the existing check in the n = 0 path.
Add explanatory comment for discarded boolean in token rule's #elif handler.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Test exercises the ifdefSkip rule's n > 0 branch with --langversion:10.0
to verify the CheckLanguageFeatureAndRecover call added in the nested
inactive #elif path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- elifIndentedTest: verifies indented #elif with leading whitespace
  compiles and runs correctly across all branches
- elifMustHaveIdentError: verifies bare #elif without expression
  produces a compilation error
- elifAllFalseNoElseTest: verifies #if/#elif chain where all
  conditions are false and no #else fallback runs with exit code 0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- elifMustBeFirstWarning: Tests #elif at non-zero column (after block comment)
  compiles successfully, exercising the shouldStartLine (FS3882) code path
- elifMustHaveIdentError: Tests bare #elif without expression triggers
  FS3883 diagnostic with proper message verification
- elifAllFalseNoElseTest: Tests all-false #if/#elif chain without #else
  compiles and runs with exit code 0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- elifMustBeFirstWarning: Tests shouldStartLine diagnostic (FS1163) for
  directive not at column 0, exercising the same code path as FS3882
- elifMustHaveIdentError: Tests FS3883 for bare #elif without expression
- elifAllFalseNoElseTest: Tests all-false #if/#elif chain with no #else

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the #if-based proxy test (FS1163) with a proper #elif test that
triggers FS3882. Use #if DEFINED with withDefines to activate the branch,
then place #elif after code on the same line to trigger shouldStartLine.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- T4+T6: Tokenizer test verifying #elif state encoding round-trip and
  inactive code classification across lines
- T5: Structure/folding test verifying fold regions for
  #if/#elif/#else/#endif chains
- T7: AST trivia test verifying ConditionalDirectiveTrivia.Elif appears
  in parsed syntax tree

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Q1: Extract evalAndSaveElif helper in lex.fsl ifdefSkip rule
- Q2: Merge Else/Elif arms in ServiceStructure.fs using or-pattern
- Q3: Unify SaveIfHash/SaveElifHash via saveConditionalHash in LexerStore.fs
- Q4: Replace stackTopRange double evaluation with nested ValueSome/ValueNone match

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- B1: elifTakenThenElseSkipped — verifies #else is skipped when prior #elif was taken
- B2: elifSecondElifAfterTakenSkipped — verifies subsequent #elif skipped after taken #elif
- B3: elifAfterElseErrorInSkipContext — verifies #elif-after-#else error via ifdefSkip path

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uses the modern FSharpToken API (not legacy FSharpLineTokenizer) to verify
FSharpTokenKind.HashElif is emitted for #elif directives.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove redundant `let m = lexbuf.LexemeRange` rebindings in #else ifdefSkip
  handler (m already in scope from line 1119)
- Inline `let isTrue = evalAndSaveElif()` into `if evalAndSaveElif() then`
- Add explanatory comment on IfDefElif arm clarifying why stack is not updated

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Clarify that forward compatibility is limited: #elif in inactive code on
  older compilers is silently ignored (langversion gate prevents in practice)
- Update ParsedInputTrivia doc comment to include #elif

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Contributor

github-actions bot commented Feb 18, 2026

⚠️ Release notes required, but author opted out

Warning

Author opted out of release notes, check is disabled for this pull request.
cc @dotnet/fsharp-team-msft

@T-Gro T-Gro added the NO_RELEASE_NOTES Label for pull requests which signals, that user opted-out of providing release notes label Feb 18, 2026
@@ -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.

@T-Gro
Copy link
Member Author

T-Gro commented Feb 18, 2026

/run fantomas

@github-actions
Copy link
Contributor

🔧 CLI Command Report

  • Command: /run fantomas
  • Outcome: success

✅ Command succeeded, no changes needed.

@T-Gro
Copy link
Member Author

T-Gro commented Feb 18, 2026

/run fantomas

@github-actions
Copy link
Contributor

🔧 CLI Command Report

  • Command: /run fantomas
  • Outcome: success

✅ Command succeeded, no changes needed.

@T-Gro
Copy link
Member Author

T-Gro commented Feb 18, 2026

/run ilverify

@github-actions
Copy link
Contributor

🔧 CLI Command Report

  • Command: /run ilverify
  • Outcome: success

✅ Command succeeded, no changes needed.

T-Gro and others added 5 commits February 18, 2026 20:27
- Move 3882,lexHashElifMustBeFirst and 3883,lexHashElifMustHaveIdent to
  end of FSComp.txt to maintain ascending error code order (they were
  placed between unnumbered entries and 1171, breaking the sort).
- Add 5 new ServiceLexing StackUnexpected ILVerify entries to both
  Debug and Release net10.0 baselines for the new #elif lexer paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The #elif lexer changes produce 5 new ServiceLexing StackUnexpected
IL entries. These need to be added to the netstandard2.0 baselines
(Debug + Release). The net10.0 baselines are reverted to their
original state as the entries added previously were from a pre-merge
CI run with different IL offsets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
netstandard2.0: Replace original 5 entries with new 5 (bootstrap build
shifts original offsets, so only the new #elif entries remain stable).
net10.0: Add 5 new entries alongside existing 5 (10 total).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The bootstrap build on CI shifts the original ServiceLexing offsets,
so only the new #elif-added entries (0x37,0x40,0x87/0x69,0x90/0x72,
0x99/0x7B) remain stable across all 4 baseline configs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@T-Gro T-Gro marked this pull request as ready for review February 19, 2026 13:50
@T-Gro T-Gro requested a review from a team as a code owner February 19, 2026 13:50
@T-Gro T-Gro requested a review from abonie February 19, 2026 13:50
@T-Gro T-Gro enabled auto-merge (squash) February 20, 2026 10:08
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

@github-project-automation github-project-automation bot moved this from New to In Progress in F# Compiler and Tooling Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

NO_RELEASE_NOTES Label for pull requests which signals, that user opted-out of providing release notes

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants