Skip to content

WIP : Experiment [this repo only] : Fast and unsafe development time compilation out of FSharpChecker caches#19267

Open
T-Gro wants to merge 27 commits into
mainfrom
feature/langserver-skill
Open

WIP : Experiment [this repo only] : Fast and unsafe development time compilation out of FSharpChecker caches#19267
T-Gro wants to merge 27 commits into
mainfrom
feature/langserver-skill

Conversation

@T-Gro

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

Copy link
Copy Markdown
Member

⚡ Fast Build from Cache — Prototype

What

Skips fsc entirely for dev builds by emitting DLLs directly from FSharpChecker's in-memory typecheck cache. Dev-loop only — no optimization, no PDB, not for shipping.

How to use

dotnet build /p:FastBuildFromCache=true

Currently scoped to FSharp.Compiler.Service and FSharp.Compiler.ComponentTests by AssemblyName guard. All other projects build normally. Falls back to normal fsc if the cache server is down.

Why it's fast

A persistent diagnostics server holds a warm FSharpChecker with useTransparentCompiler=true. The TransparentCompiler 📚 caches typecheck results per-file, keyed by content hashes. When a file changes:

  • A dependency 🔀 graph built at parse time (DependencyResolution.mkGraph) determines which files actually depend on the changed one — via a trie of top-level namespaces/modules and open resolution
  • Only those files get re-typechecked; everything else is served from AsyncMemoize 📚 caches (TcIntermediate, ParseFile, DependencyGraph)
  • The new FSharpChecker.CompileFromCheckedProject then goes straight from 📚 cached CheckedImplFile[] → ILX codegen → WriteILBinaryFile → DLL, skipping parse/check/optimize

Edit file 300 of 420 → files 1–299 stay cached, and peers with no dependency edge to file 300 also stay cached.

Key pieces

What Where
MSBuild interception eng/targets/FastBuildFromCache.targetsBeforeTargets="CoreCompile", sets SkipCompilerExecution=true on success
📚 Cache→DLL API FSharpChecker.CompileFromCheckedProject in service.fs
Server compile handler Server.fs"compile" command
🔀 Project routing ProjectRouting.fs — maps source files → .fsproj
📚 Multi-project cache ProjectManager.fsConcurrentDictionary keyed by fsproj path + mtime

T-Gro added 15 commits February 8, 2026 20:27
Add internal CompilationData member on FSharpCheckProjectResults to expose
cached typecheck data (TcConfig, TcGlobals, TcImports, CcuThunk, TopAttribs,
ILAssemblyRef, CheckedImplFiles).

Add public CompileFromCheckedProject method on FSharpChecker that takes check
results and an output path, then runs the backend pipeline (sigdata encoding,
IL generation, module creation, binary writing) without re-parsing or
re-typechecking. Skips optimization and PDB generation for fast dev-loop use.
Save and restore generatedCcu.Contents.Attribs around the compilation
to prevent repeated CompileFromCheckedProject calls from appending
assembly attributes to the shared cached CCU on each invocation.

Includes regression test and surface area baseline update.
Add a 'compile' case to the handleRequest match block in Server.fs that:
- Extracts project and output paths from JSON
- Resolves project options via projectMgr
- Runs ParseAndCheckProject and checks HasCriticalErrors
- Calls CompileFromCheckedProject on success
- Returns 'OK' on success, 'ERROR: ...' on failure
Use Proto-built FSharp.Compiler.Service.dll (from artifacts/Bootstrap/fsc/)
when available, falling back to NuGet PackageReference otherwise. This allows
the diagnostics server to access CompileFromCheckedProject API from current
source before it ships in a release.
- ProjectManager: Replace single option cache with Dictionary<string, DateTime * FSharpProjectOptions>
- ProjectManager: Add path normalization via Path.GetFullPath
- ProjectManager: Invalidate accepts optional fsproj path (one or all)
- Server: Add resolveProject helper mapping source files to fsproj paths
- Server: getOptions now accepts file path parameter
- Server: All handlers (parseOnly, check, findRefs, typeHints) pass file to getOptions
- Server: checkProject accepts optional 'project' JSON field, defaults to FCS fsproj
- Server: Remove hardcoded fsproj variable
- ProjectManager.fs: Dictionary<string, DateTime * FSharpProjectOptions> cache
  with path normalization and optional Invalidate(?fsprojPath)
- ProjectRouting.fs: extracted resolveProject for testability, maps source
  files to fsproj (ComponentTests or FCS)
- Server.fs: uses ProjectRouting.resolveProject, all handlers route via file path
- checkProject accepts optional 'project' JSON field, defaults to FCS fsproj
- Tests: 10 tests covering resolveProject mapping and ProjectManager.Invalidate
- Add CacheCount, HasCachedProject, InjectTestEntry to ProjectManager for testability
- Rewrite ProjectManagerTests with behavioral assertions (cache population, selective/full invalidation, path normalization)
- Refactor ResolveProjectTests to use Theory/InlineData, eliminating copy-paste
- Shared createManager/dummyOptions helpers eliminate repeated setup
- 13 tests: 7 ProjectManager + 6 routing (all pass)
… tests

- Mark CacheCount, HasCachedProject, InjectTestEntry as internal
- Add InternalsVisibleTo for test project in server fsproj
- Add test: Invalidate normalizes path before removal
- Add test: InjectTestEntry overwrites existing entry (idempotency)
- Add test: resolveProject with out-of-repo file defaults to FCS
- Update guard condition in FastBuildFromCache.targets to also match
  AssemblyName=='FSharp.Compiler.ComponentTests'
- Add Import of FastBuildFromCache.targets to FSharpTests.Directory.Build.targets
  (mirrors existing import in FSharpBuild.Directory.Build.targets)
- Fix XML comment containing '--compile' (invalid in XML comments)
- Update header/guard comments to mention ComponentTests
CODE-QUALITY:
- Fix unused outPath binding in Server.fs compile handler (let! _ =)
- Improve ProjectRouting.resolveProject to use StartsWith prefix check
  instead of fragile String.Replace, with StringComparison.Ordinal
- Add XML doc comment to resolveProject

NO-LEFTOVERS:
- Remove stale src/FastBuildFromCache.targets (old approach, superseded by eng/targets/)
- Remove leftover plan files (FAST_COMPILE_PLAN.md, REUSABLE_COMPILE_PLAN.md)
- Revert incorrect Directory.Build.targets import (wrong path)
- Add missing FSharpBuild.Directory.Build.targets import for src/ projects
- Include uncommitted shell script --compile handler and SKILL.md docs

TEST-COVERAGE:
- Add DesignTimeBuildTests for config defaults and DtbResult construction
- Add ProjectRouting tests: vsintegration path, FSharp.Core path,
  trailing slash, repoRoot-as-substring edge case
- Add ProjectManager tests: 3-project coexistence, selective invalidation

TEST-CODE-QUALITY:
- Tests use descriptive names and verify specific behaviors
- Edge case tests validate the String.Replace fix

All 24 tests pass. Server builds with 0 warnings.
…mCheckedProject internal

CODE-QUALITY: Extract normalizeAssemblyRefs as TcImports.NormalizeAssemblyRef
member to eliminate duplication between service.fs and fsc.fs. Make
CompileFromCheckedProject internal since it's a dev-loop-only API.
Add InternalsVisibleTo for FSharpDiagServer.

NO-LEFTOVERS: Remove redundant comment in ResolveProjectTests.fs.
Unstage .ralph/ tracking files.

Surface area baseline updated to reflect internal visibility change.
…, TEST-CODE-QUALITY, TEST-COVERAGE verifier feedback

- CODE-QUALITY: Break long line in Server.fs, use InvalidOperationException
  instead of failwith in CompileFromCheckedProject, add project-not-found
  validation in compile handler, add caching semantics comment
- HONEST-ASSESSMENT: Update SKILL.md to reflect ComponentTests support
- NO-LEFTOVERS: Remove unused InternalsVisibleTo from FSharpDiagServer.fsproj,
  update shell script usage/header to include --compile flag
- TEST-CODE-QUALITY: Consolidate duplicate Fact tests into Theory InlineData,
  add meaningful DesignTimeBuildTests for config overrides and edge cases
- TEST-COVERAGE: Add ResolveProjectOptions error path test, add FCS fallback
  and ComponentTests boundary routing tests, add DtbConfig edge cases
PERF:
- Replace Dictionary+lock with ConcurrentDictionary in ProjectManager
  to eliminate lock contention on concurrent cache lookups
- Use HashSet instead of ResizeArray for O(1) symbol name lookup in findRefs
- Avoid Array.append allocation when one diagnostics array is empty

TEST-COVERAGE:
- Add concurrent InjectTestEntry+Invalidate thread safety test
- Add concurrent InjectTestEntry from multiple threads test
- Add Invalidate-during-concurrent-reads test
- Add error-does-not-pollute-cache test
- Include leftover test consolidation from Fixup #2
@github-actions

github-actions Bot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

T-Gro added 10 commits February 10, 2026 13:38
… filewatcher pre-warming, --times profiling

- NormalizeAssemblyRef on TcImports is now member internal (no public API change)
- CompileFromCheckedProject uses minimal optimizer with mandatory lowering passes
  (OptimizeImplFile + LowerLocalMutables + LowerCalls, no detuple/TLR/extra loops)
- Added ReportTime instrumentation for --times profiling of emit phases
- FastBuildFromCache.targets: added Inputs/Outputs mirroring CoreCompile for
  proper MSBuild incremental skip, touch all CoreCompile outputs after cache emit
- Diagnostics server: filewatcher pre-warming with 5s throttle on src/Compiler/
- Diagnostics server: compile handler with --compile flag, DTB caching, resource embedding
…iateOutputPath field

- git rm --cached 99_CI_Fixup_ReleaseNotes.md (NO-LEFTOVERS)
- Add IntermediateOutputPath to DtbResult constructions in DesignTimeBuildTests.fs (TEST-CODE-QUALITY)
@T-Gro T-Gro marked this pull request as ready for review February 13, 2026 18:29
@T-Gro T-Gro requested a review from a team as a code owner February 13, 2026 18:29
T-Gro and others added 2 commits April 13, 2026 13:12
Resolve merge conflict in src/Compiler/Service/service.fs:
kept both new imports (ILBinaryWriter, CheckExpressionsOps from PR,
Caches from main).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The release notes version bumped to 11.0.100 after merging main.
Move the PR #19267 entry to the correct file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jul 2, 2026
Per-edit hot reload currently requires an external 'dotnet build -t:Compile' to
refresh the obj assembly the delta emitter reads, making every edit pay a full
MSBuild+fsc invocation on top of the in-process check (measured ~7s/edit vs
~4.7s for a plain build). This adds an experimental, flag-gated path that
produces the same on-disk obj DLL and PDB in-process from the checked project
the session already has, collapsing the two compiles into one (spike-measured
~3.0s/edit with a correct one-method delta).

- FSharpChecker.CompileFromCheckedProject (internal): emits the assembly from
  cached typecheck results (finalize CCU, dedupe QualifiedNameOfFile, optimize,
  IlxGen, write DLL + portable PDB to the requested path), adapted from the
  dotnet#19267 prototype. Before codegen it clears leaked hot-reload
  closure-name state and resets the compiler-generated-name counters
  (ResetCompilerGeneratedNameState) so the emit lays out generated names exactly
  like a fresh-process build; without this an unchanged closure drifts to a new
  occurrence suffix and the delta mapping degenerates.
- FSharpCheckProjectResults.CompilationData and TcImports.NormalizeAssemblyRef
  (internal) supply the compilation inputs, from the same prototype.
- ResetCompilerGeneratedNameState on the name generators, matching
  dotnet#20017 so the branch converges with main when that PR lands.
- Session wiring: when FSHARP_HOTRELOAD_INPROCESS_COMPILE is truthy,
  EmitHotReloadDelta runs the in-process compile after session-active
  validation and before the output freshness pipeline, failing closed as
  DeltaEmissionFailed on any compile error. With the flag unset the code path
  is unchanged beyond a single environment read. The external build remains
  the default; dotnet-watch integration opts in separately.
- The emitted portable PDB keeps the sibling-PDB freshness contract: sequence
  points for line-shifted, unedited methods come from the new compile, not a
  stale external-build PDB (regression-tested; the test fails without the PDB
  emission).

Verified: service HotReload suite 414 passed 0 failed (including the new
flag on/off contract test asserting one updated method with no external
rebuild, a +1 LineUpdate for an unedited shifted method, and the unchanged
stale-output refusal with the flag off); component HotReload suite 229 passed
0 failed (flag-off neutrality).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

1 participant