From ae5cb59c5f5670d50980f98e87be32c062695a64 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Thu, 10 Sep 2026 09:47:28 +0200 Subject: [PATCH 1/3] Fix FCS docs scripts broken by API changes The literate scripts under docs/fcs no longer compiled against the current FSharp.Compiler.Service, so the published tutorial pages rendered compiler errors instead of output. fsdocs 23 surfaces script evaluation failures rather than discarding them, which made this visible. - tokenizer.fsx: FSharpSourceTokenizer takes three arguments now, drop the trailing None and describe the language version argument. - editor.fsx: FSharpMethodGroupItemParameter.Display is a RichText, so read its Text instead of enumerating tagged parts. - typedtree.fsx: annotate the input as string to disambiguate the new File.WriteAllText(string, ReadOnlySpan) overload. - untypedtree.fsx: SynExpr.LetOrUse is a SynLetOrUse record and SynModuleDecl.Let gained a trivia field. - untypedtree-apis.fsx: SynComponentInfo lost its longId field in #19602, use the LongIdent compatibility member. --- docs/fcs/editor.fsx | 4 +--- docs/fcs/tokenizer.fsx | 12 ++++++------ docs/fcs/typedtree.fsx | 2 +- docs/fcs/untypedtree-apis.fsx | 12 ++++++------ docs/fcs/untypedtree.fsx | 4 ++-- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/docs/fcs/editor.fsx b/docs/fcs/editor.fsx index 167c408ee11..ce3dce9323a 100644 --- a/docs/fcs/editor.fsx +++ b/docs/fcs/editor.fsx @@ -215,9 +215,7 @@ let methods = // Print concatenated parameter lists for mi in methods.Methods do - [ for p in mi.Parameters do - for tt in p.Display do - yield tt.Text ] + [ for p in mi.Parameters -> p.Display.Text ] |> String.concat ", " |> printfn "%s(%s)" methods.MethodName (** diff --git a/docs/fcs/tokenizer.fsx b/docs/fcs/tokenizer.fsx index 939afb37e9c..690374e64ce 100644 --- a/docs/fcs/tokenizer.fsx +++ b/docs/fcs/tokenizer.fsx @@ -30,13 +30,13 @@ To use the tokenizer, reference `FSharp.Compiler.Service.dll` and open the #r "FSharp.Compiler.Service.dll" open FSharp.Compiler.Tokenization (** -Now you can create an instance of `FSharpSourceTokenizer`. The class takes two -arguments - the first is the list of defined symbols and the second is the -file name of the source code. The defined symbols are required because the -tokenizer handles `#if` directives. The file name is required only to specify -locations of the source code (and it does not have to exist): +Now you can create an instance of `FSharpSourceTokenizer`. The class takes three +arguments - the first is the list of defined symbols, the second is the +file name of the source code and the third is the language version. The defined symbols +are required because the tokenizer handles `#if` directives. The file name is required only +to specify locations of the source code (and it does not have to exist): *) -let sourceTok = FSharpSourceTokenizer([], Some "C:\\test.fsx", Some "PREVIEW", None) +let sourceTok = FSharpSourceTokenizer([], Some "C:\\test.fsx", Some "PREVIEW") (** Using the `sourceTok` object, we can now (repeatedly) tokenize lines of F# source code. diff --git a/docs/fcs/typedtree.fsx b/docs/fcs/typedtree.fsx index 476fbe832bf..ad52bb063db 100644 --- a/docs/fcs/typedtree.fsx +++ b/docs/fcs/typedtree.fsx @@ -48,7 +48,7 @@ One difference is that we set keepAssemblyContents to true. // Create an interactive checker instance let checker = FSharpChecker.Create(keepAssemblyContents=true) -let parseAndCheckSingleFile (input) = +let parseAndCheckSingleFile (input: string) = let file = Path.ChangeExtension(System.IO.Path.GetTempFileName(), "fsx") File.WriteAllText(file, input) // Get context representing a stand-alone (script) file diff --git a/docs/fcs/untypedtree-apis.fsx b/docs/fcs/untypedtree-apis.fsx index c713ad8acff..86ca5694161 100644 --- a/docs/fcs/untypedtree-apis.fsx +++ b/docs/fcs/untypedtree-apis.fsx @@ -363,8 +363,8 @@ let outermostNestedModule = // Some ["N"]. (posInsideOfInnermostNestedModule, mkTree nestedModules) ||> ParsedInput.tryPick (fun _path node -> match node with - | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longId))) -> - Some [for ident in longId -> ident.idText] + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = moduleInfo)) -> + Some [for ident in moduleInfo.LongIdent -> ident.idText] | _ -> None) (** @@ -378,8 +378,8 @@ let innermostNestedModule = // Some ["P"]. (posInsideOfInnermostNestedModule, mkTree nestedModules) ||> ParsedInput.tryPickLast (fun _path node -> match node with - | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longId))) -> - Some [for ident in longId -> ident.idText] + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = moduleInfo)) -> + Some [for ident in moduleInfo.LongIdent -> ident.idText] | _ -> None) (** @@ -391,8 +391,8 @@ let nextToInnermostNestedModule = // Some ["O"]. ||> ParsedInput.tryPickLast (fun path node -> match node, path with | SyntaxNode.SynModule(SynModuleDecl.NestedModule _), - SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longId))) :: _ -> - Some [for ident in longId -> ident.idText] + SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = moduleInfo)) :: _ -> + Some [for ident in moduleInfo.LongIdent -> ident.idText] | _ -> None) (** diff --git a/docs/fcs/untypedtree.fsx b/docs/fcs/untypedtree.fsx index fc63ab467e7..09eca22740d 100644 --- a/docs/fcs/untypedtree.fsx +++ b/docs/fcs/untypedtree.fsx @@ -141,7 +141,7 @@ let rec visitExpression e = visitExpression trueBranch falseBranchOpt |> Option.iter visitExpression - | SynExpr.LetOrUse(_, _, bindings, body, _, _) -> + | SynExpr.LetOrUse { Bindings = bindings; Body = body } -> // Visit bindings (there may be multiple // for 'let .. = .. and .. = .. in ...' printfn "LetOrUse with the following bindings:" @@ -172,7 +172,7 @@ functions): let visitDeclarations decls = for declaration in decls do match declaration with - | SynModuleDecl.Let(isRec, bindings, range) -> + | SynModuleDecl.Let(bindings = bindings) -> // Let binding as a declaration is similar to let binding // as an expression (in visitExpression), but has no body for binding in bindings do From 9c32bbae8eeff713b63955510320a33998134cf7 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Thu, 10 Sep 2026 10:31:04 +0200 Subject: [PATCH 2/3] Map release notes to the packages that actually shipped The release notes pages derived the package version from the notes file name, which no longer holds. FSharp.Compiler.Service bumps its minor version independently of the F# version (43.12.100 is F# 11.0.100 while 43.12.204 is F# 10.0.204), and FSharp.Core 10.0.2xx and 10.0.3xx shipped as 10.1.x. Recent headings therefore named versions that do not exist and showed as unreleased. Each package on NuGet is now matched to its notes file through the source commit recorded in its nuspec: eng/Versions.props at that commit (under src/fsharp for VMR builds) gives the exact F# version. The heading shows the first package that shipped for the notes, with a badge per servicing rebuild, and the page is ordered by package version. Also: - Sort versions numerically instead of as strings, so 10.x and 11.x no longer sort below 9.x. The Language page had a broken comparer that threw on three-part versions and rendered nothing. - Distinguish unlisted packages, which NuGet reports with a 1900-01-01 publish date, from ones that are not on NuGet at all. - Silence FsHttp request logging and bump Markdig and FsHttp. --- docs/release-notes/.aux/Common.fsx | 232 ++++++++++++++++-- .../release-notes/FSharp.Compiler.Service.fsx | 39 +-- docs/release-notes/FSharp.Core.fsx | 27 +- docs/release-notes/Language.fsx | 17 +- 4 files changed, 233 insertions(+), 82 deletions(-) diff --git a/docs/release-notes/.aux/Common.fsx b/docs/release-notes/.aux/Common.fsx index a64026d2934..a74d874c5cb 100644 --- a/docs/release-notes/.aux/Common.fsx +++ b/docs/release-notes/.aux/Common.fsx @@ -1,12 +1,16 @@ #i "nuget: https://api.nuget.org/v3/index.json" -#r "nuget: Markdig, 0.33.0" -#r "nuget: FsHttp, 12.1.0" +#r "nuget: Markdig, 1.3.2" +#r "nuget: FsHttp, 15.0.3" open System.IO open System.Xml.Linq open System.Text.RegularExpressions +open Markdig open FsHttp +// FsHttp logs every request to the console in FSI, which is noise in the docs build. +Fsi.disableDebugLogs () + let versionProps = Path.Combine(__SOURCE_DIRECTORY__, "../../../eng/Versions.props") let versionPropsDoc = XDocument.Load(versionProps) @@ -19,18 +23,130 @@ let getAvailableNuGetVersions (packageName: string) : Set = |> Response.deserializeJson<{| versions: string array |}> |> fun json -> Set.ofArray json.versions -/// Try and find the publish date on NuGet -let tryGetReleaseDate (packageName: string) (version: string) : string option = - let packageName = packageName.ToLowerInvariant() +/// How a version of a package stands on NuGet +type NuGetRelease = + /// The package version does not exist on NuGet + | Unreleased + /// The package version exists but the owner unlisted it. NuGet reports 1900-01-01 as its + /// publish date, so the date is meaningless. + | Unlisted + /// Published on the given date (yyyy-MM-dd) + | Published of date: string - http { GET $"https://api.nuget.org/v3/registration5-gz-semver2/%s{packageName}/%s{version}.json" } - |> Request.send - |> Response.deserializeJson<{| published: string |}> - |> fun json -> - if System.String.IsNullOrWhiteSpace json.published then - None - else - Some(json.published.Split('T').[0]) +/// Find out whether a version of a package is published on NuGet, and when +let getRelease (packageName: string) (availableVersions: Set) (version: string) : NuGetRelease = + if not (availableVersions.Contains version) then + Unreleased + else + let packageName = packageName.ToLowerInvariant() + + http { GET $"https://api.nuget.org/v3/registration5-gz-semver2/%s{packageName}/%s{version}.json" } + |> Request.send + |> Response.deserializeJson<{| published: string; listed: bool |}> + |> fun json -> + if not json.listed then Unlisted + elif System.String.IsNullOrWhiteSpace json.published then Unreleased + else Published(json.published.Split('T').[0]) + +/// The heading for a version: the version and its release date or status +let releaseTitle (version: string) (release: NuGetRelease) : string = + match release with + | Unreleased -> $"%s{version} - Not on NuGet" + | Unlisted -> $"%s{version} - Unlisted on NuGet" + | Published date -> $"%s{version} - %s{date}" + +/// A badge linking to the package version on NuGet +let nugetBadgeLink (packageName: string) (version: string) : string = + $"\"Nuget\"" + +/// A badge linking to the package version on NuGet, for versions that exist there +let nugetBadge (packageName: string) (version: string) (release: NuGetRelease) : string = + match release with + | Unreleased -> System.String.Empty + | Unlisted + | Published _ -> nugetBadgeLink packageName version + +/// The F# version a package was built from, as FSMajor.FSMinor.FSBuild in eng/Versions.props of +/// the source commit that the nuspec on NuGet records. Packages are built from dotnet/fsharp or, +/// since .NET 10, from the dotnet/dotnet VMR where the repo lives under src/fsharp. +let tryGetSourceFSharpVersion (packageName: string) (version: string) : Async = + async { + try + let packageName = packageName.ToLowerInvariant() + + let! nuspec = + http { GET $"https://api.nuget.org/v3-flatcontainer/%s{packageName}/%s{version}/%s{packageName}.nuspec" } + |> Request.sendAsync + + let nuspec = nuspec |> Response.toText + let repository = Regex.Match(nuspec, "]*url=\"([^\"]+)\"[^>]*commit=\"([0-9a-f]+)\"") + + if not repository.Success then + eprintfn "%s %s: no source repository in nuspec" packageName version + return None + else + let url = repository.Groups.[1].Value.TrimEnd('/') + let commit = repository.Groups.[2].Value + + let versionsProps = + if url.EndsWith "dotnet/dotnet" then + $"https://raw.githubusercontent.com/dotnet/dotnet/%s{commit}/src/fsharp/eng/Versions.props" + else + $"https://raw.githubusercontent.com/dotnet/fsharp/%s{commit}/eng/Versions.props" + + let! props = http { GET versionsProps } |> Request.sendAsync + let doc = XDocument.Parse(props |> Response.toText) + // The first element wins: FSMinorVersion is redefined further down for FSharp.Core only. + let value name = (doc.Descendants(XName.Get name) |> Seq.head).Value + let major = value "FSMajorVersion" + let minor = value "FSMinorVersion" + let build = value "FSBuildVersion" + return Some $"%s{major}.%s{minor}.%s{build}" + with ex -> + eprintfn "%s %s: could not determine the source F# version: %s" packageName version ex.Message + return None + } + +/// The release notes file an F# version belongs to: the highest notes version at or below it in +/// the same hundreds band. So with files 9.0.200 and 9.0.202, F# 9.0.201 belongs to 9.0.200 and +/// F# 9.0.203 to 9.0.202, while 9.0.303 belongs to 9.0.300. +let releaseNotesVersionOf (notesVersions: string seq) (fsharpVersion: string) : string option = + let version = System.Version.Parse fsharpVersion + + notesVersions + |> Seq.map System.Version.Parse + |> Seq.filter (fun notes -> + notes.Major = version.Major + && notes.Minor = version.Minor + && notes.Build / 100 = version.Build / 100 + && notes <= version) + |> Seq.sortDescending + |> Seq.tryHead + |> Option.map string + +/// Groups package versions on NuGet by the release notes file they belong to, looking up the +/// F# version each package was built from. Versions within a group are sorted ascending. +let getPackagesByReleaseNotes + (packageName: string) + (notesVersions: string seq) + (versions: string seq) + : Map = + let numericPrefix (version: string) = + System.Version.Parse(version.Split('-').[0]) + + versions + |> Seq.map (fun version -> + async { + let! fsharpVersion = tryGetSourceFSharpVersion packageName version + return fsharpVersion |> Option.bind (releaseNotesVersionOf notesVersions) |> Option.map (fun notes -> notes, version) + }) + |> fun lookups -> Async.Parallel(lookups, maxDegreeOfParallelism = 8) + |> Async.RunSynchronously + |> Seq.choose id + |> Seq.groupBy fst + |> Seq.map (fun (notes, packages) -> + notes, packages |> Seq.map snd |> Seq.sortBy numericPrefix |> List.ofSeq) + |> Map.ofSeq /// In order for the heading to appear in the page content menu in fsdocs, /// they need to follow a specific HTML structure. @@ -42,9 +158,95 @@ let transformH3 (version: string) (input: string) : string = Regex.Replace(input, pattern, replacement) -/// Process all MarkDown files from the given release folder +/// Orders release note file names newest first. Version numbers compare numerically, so that +/// 10.0.100 comes before 9.0.300. Names that are not a version, such as "preview" or "18.vNext", +/// are the upcoming release and go on top. +let compareVersionsDescending (a: string) (b: string) : int = + let tryParse (name: string) = + match System.Version.TryParse name with + | true, version -> Some version + | _ -> None + + match tryParse a, tryParse b with + | Some a, Some b -> compare b a + | Some _, None -> 1 + | None, Some _ -> -1 + | None, None -> compare b a + +/// The F# version main is at, and the FCS version that goes with it +let upcomingFSharpVersion, upcomingFcsVersion = + let value name = (versionPropsDoc.Descendants(XName.Get name) |> Seq.head).Value + let build = value "FSBuildVersion" + System.Version.Parse $"""{value "FSMajorVersion"}.{value "FSMinorVersion"}.{build}""", + System.Version.Parse $"""{value "FCSMajorVersion"}.{value "FCSMinorVersion"}.{build}""" + +/// Renders the release notes of a package: one section per notes file in `path`, ordered by +/// package version, newest first. Which package versions belong to a notes file is looked up +/// from the source commit of each package on NuGet, from `minPackageVersion` on. +/// +/// Notes without a package are either the upcoming release, placed at the top as `upcomingVersion`, +/// or an old servicing version that never shipped, placed where `packageVersionOfNotes` puts it. +let renderPackageReleaseNotes + (packageName: string) + (path: string) + (minPackageVersion: System.Version) + (packageVersionOfNotes: System.Version -> System.Version) + (upcomingVersion: System.Version) + : string = + let numericPrefix (version: string) = + System.Version.Parse(version.Split('-').[0]) + + let availableVersions = getAvailableNuGetVersions packageName + + let notesVersions = + Directory.EnumerateFiles(path, "*.md") |> Seq.map Path.GetFileNameWithoutExtension |> List.ofSeq + + let packagesByReleaseNotes = + availableVersions + |> Seq.filter (fun version -> numericPrefix version >= minPackageVersion) + |> getPackagesByReleaseNotes packageName notesVersions + + notesVersions + |> List.map (fun notesVersion -> + let file = Path.Combine(path, notesVersion + ".md") + let packages = packagesByReleaseNotes.TryFind notesVersion |> Option.defaultValue [] + let stable = packages |> List.filter (fun version -> not (version.Contains '-')) + + // The heading is the first package that shipped for these notes; later ones are servicing + // rebuilds and get a badge each. Without a stable package, show the latest prerelease. + let version, title, badges = + match stable, List.rev packages with + | first :: _, _ -> + let release = getRelease packageName availableVersions first + let badges = stable |> List.map (nugetBadgeLink packageName) + first, releaseTitle first release, String.concat " " badges + | [], latestPrerelease :: _ -> + latestPrerelease, $"%s{latestPrerelease} - Prerelease", nugetBadgeLink packageName latestPrerelease + | [], [] -> notesVersion, $"F# %s{notesVersion} - Not on NuGet", System.String.Empty + + let sortKey = + match packages with + | _ :: _ -> numericPrefix version + | [] -> + let notes = System.Version.Parse notesVersion + + if notes >= upcomingFSharpVersion then + upcomingVersion + else + packageVersionOfNotes notes + + let content = File.ReadAllText file |> Markdown.ToHtml |> transformH3 version + + sortKey, + $"""

%s{title}

%s{badges}%s{content}""") + |> List.sortByDescending fst + |> List.map snd + |> String.concat "\n" + +/// Process all MarkDown files from the given release folder, newest version first let processFolder (path: string) (processFile: string -> string) : string = Directory.EnumerateFiles(path, "*.md") - |> Seq.sortByDescending Path.GetFileNameWithoutExtension + |> Seq.sortWith (fun a b -> + compareVersionsDescending (Path.GetFileNameWithoutExtension a) (Path.GetFileNameWithoutExtension b)) |> Seq.map processFile |> String.concat "\n" diff --git a/docs/release-notes/FSharp.Compiler.Service.fsx b/docs/release-notes/FSharp.Compiler.Service.fsx index fc116d572cf..7be7af2945e 100644 --- a/docs/release-notes/FSharp.Compiler.Service.fsx +++ b/docs/release-notes/FSharp.Compiler.Service.fsx @@ -11,37 +11,20 @@ title: FSharp.Compiler.Service #load "./.aux/Common.fsx" open System.IO -open System.Xml.XPath +open System.Xml.Linq open Markdig open Common let path = Path.Combine(__SOURCE_DIRECTORY__, ".FSharp.Compiler.Service") -let fcsMajorVersion = versionPropsDoc.XPathSelectElement("//FCSMajorVersion").Value -let nugetPackage = "FSharp.Compiler.Service" -let availableNuGetVersions = getAvailableNuGetVersions nugetPackage -processFolder path (fun file -> - let versionInFileName = Path.GetFileNameWithoutExtension(file) - // Example: 8.0.200 - let versionParts = versionInFileName.Split '.' - - let version = $"%s{fcsMajorVersion}.%s{versionParts.[0]}.%s{versionParts.[2]}" - // TODO: Can we determine if the current version is in code freeze based on the Version.props info? - let title = - if not (availableNuGetVersions.Contains version) then - $"%s{version} - Unreleased" - else - match tryGetReleaseDate nugetPackage version with - | None -> $"%s{version} - Unreleased" - | Some d -> $"%s{version} - %s{d}" - - let nugetBadge = - if not (availableNuGetVersions.Contains version) then - System.String.Empty - else - $"\"Nuget\"" - - let content = File.ReadAllText file |> Markdown.ToHtml |> transformH3 version - - $"""

%s{title}

%s{nugetBadge}%s{content}""") +// The FCS package version cannot be derived from the release notes file name: its minor number +// is bumped independently of the F# version (43.12.100 is F# 11.0.100, 43.12.204 is F# 10.0.204). +// The lookup by source commit handles that; the F# major is only used to place notes that never +// shipped. Packages before 43.8 predate the release notes folder. +renderPackageReleaseNotes + "FSharp.Compiler.Service" + path + (System.Version(43, 8, 0)) + (fun notes -> System.Version(43, notes.Major, notes.Build)) + upcomingFcsVersion (*** include-it-raw ***) diff --git a/docs/release-notes/FSharp.Core.fsx b/docs/release-notes/FSharp.Core.fsx index 20fec5f86ed..224212ed75f 100644 --- a/docs/release-notes/FSharp.Core.fsx +++ b/docs/release-notes/FSharp.Core.fsx @@ -15,28 +15,9 @@ open Markdig open Common let path = Path.Combine(__SOURCE_DIRECTORY__, ".FSharp.Core") -let nugetPackage = "FSharp.Core" -let availableNuGetVersions = getAvailableNuGetVersions nugetPackage -processFolder path (fun file -> - let version = Path.GetFileNameWithoutExtension(file) - - // TODO: Can we determine if the current version is in code freeze based on the Version.props info? - let title = - if not (availableNuGetVersions.Contains version) then - $"%s{version} - Unreleased" - else - match tryGetReleaseDate nugetPackage version with - | None -> $"%s{version} - Unreleased" - | Some d -> $"%s{version} - %s{d}" - - let nugetBadge = - if not (availableNuGetVersions.Contains version) then - System.String.Empty - else - $"\"Nuget\"" - - let content = File.ReadAllText file |> Markdown.ToHtml |> transformH3 version - - $"""

%s{title}

%s{nugetBadge}%s{content}""") +// FSharp.Core mostly follows the F# version, but not always: the F# 10 packages shipped as 10.1.x +// to signal breaking changes, while the notes file stays 10.0.x. The lookup by source commit +// handles that. Packages before 8.0 predate the release notes folder. +renderPackageReleaseNotes "FSharp.Core" path (System.Version(8, 0, 0)) id upcomingFSharpVersion (*** include-it-raw ***) diff --git a/docs/release-notes/Language.fsx b/docs/release-notes/Language.fsx index 3bcd6f14c3d..dfddcf32037 100644 --- a/docs/release-notes/Language.fsx +++ b/docs/release-notes/Language.fsx @@ -16,24 +16,9 @@ open Common let path = Path.Combine(__SOURCE_DIRECTORY__, ".Language") -Directory.EnumerateFiles(path, "*.md") -|> Seq.sortWith (fun a b -> - let a = Path.GetFileNameWithoutExtension a - let b = Path.GetFileNameWithoutExtension b - - match a, b with - | "preview", "preview" -> 0 - | "preview", _ -> -1 - | _, "preview" -> 1 - | _, _ -> - match System.Decimal.TryParse(b), System.Decimal.TryParse(b) with - | (true, a) , ( true, b) -> compare (int b) (int a) - | _ -> failwithf "Cannot compare %s with %s" b a - ) -|> Seq.map (fun file -> +processFolder path (fun file -> let version = Path.GetFileNameWithoutExtension(file) let version = if version = "preview" then "Preview" else version let content = File.ReadAllText file |> Markdown.ToHtml |> transformH3 version $"""

%s{version}

%s{content}""") -|> String.concat "\n" (*** include-it-raw ***) From 186265af972bb73623192ff150dd4b3a642e5819 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Thu, 10 Sep 2026 10:37:14 +0200 Subject: [PATCH 3/3] Give every docs page a category in the published navigation Twelve pages had no fsdocs front matter, so the published site listed them in an unnamed "Other" group at the bottom of the navigation. The postmortems get their own "Postmortems" category, with the README as the overview. The remaining pages join the existing categories: the equality optimizations, regression testing, LabelOps, perf archive and SRTP guide under Compiler Internals, the LSP proposal under Language Service Internals, reflection-free printing under FSharp.Core and the pending breaking changes under Release Notes. The postmortem skill now tells the agent to emit that front matter, and the postmortems README no longer links to ../../.github, which cannot resolve on the site. The "running the documentation locally" guide is rewritten for the build.fsx pipeline in fsharp-compiler-docs and its FSHARP_REPO variable, and documents how fsdocs watch caches pages and why edits to the release-notes markdown are not picked up. --- .github/skills/postmortem/SKILL.md | 13 +++++- docs/labelops.md | 6 +++ docs/lsp.md | 6 +++ docs/optimizations-equality.md | 6 +++ docs/perf-discussions-archive.md | 6 +++ docs/postmortems/README.md | 8 +++- .../regression-fs0229-bstream-misalignment.md | 6 +++ ...gacy-inline-metadata-dynamic-invocation.md | 6 +++ ...n-parse-tree-fidelity-return-attributes.md | 6 +++ ...n-sourcebuild-cpm-runtime-version-floor.md | 6 +++ docs/reflectionfree-printing.md | 6 +++ docs/regression-testing-pipeline.md | 6 +++ .../release-notes/PENDING_BREAKING_CHANGES.md | 6 +++ docs/running-documentation-locally.md | 42 ++++++++++++------- docs/srtp-guide.md | 4 ++ 15 files changed, 116 insertions(+), 17 deletions(-) diff --git a/.github/skills/postmortem/SKILL.md b/.github/skills/postmortem/SKILL.md index 035862c07bb..b6d722d8285 100644 --- a/.github/skills/postmortem/SKILL.md +++ b/.github/skills/postmortem/SKILL.md @@ -32,6 +32,17 @@ Before writing a single line, answer these questions: Write the file in `docs/postmortems/` with a descriptive filename (e.g., `regression-fs0229-bstream-misalignment.md`). +Start the file with fsdocs front matter so the page lands under "Postmortems" in the navigation of the published docs at https://fsharp.github.io/fsharp-compiler-docs/. Without it, the page ends up in an unnamed group. Keep `title` short (it is the navigation entry; the `# Regression: ...` heading below stays descriptive), and pick the next free `index` after the existing postmortems: + +```yaml +--- +title: FS0229 B-stream misalignment +category: Postmortems +categoryindex: 550 +index: 600 +--- +``` + Use this outline: ### Summary @@ -72,4 +83,4 @@ What has been or should be added to prevent recurrence: tests, agentic instructi 3. **Do not create instructions without path scoping.** A postmortem lesson that applies "everywhere" is too vague to be actionable. If you can't name the files where the lesson matters, the postmortem may not meet the threshold for this skill. -4. **Update `docs/postmortems/README.md`** if it maintains an index. +4. **Update `docs/postmortems/README.md`**, which maintains an index. Links there must be relative to the file or absolute GitHub URLs; paths that climb out of `docs/` (such as `../../.github/`) do not resolve on the published site. diff --git a/docs/labelops.md b/docs/labelops.md index a943b84554f..57819e6b032 100644 --- a/docs/labelops.md +++ b/docs/labelops.md @@ -1,3 +1,9 @@ +--- +title: LabelOps +category: Compiler Internals +categoryindex: 200 +index: 960 +--- # LabelOps Opt-in, label-gated agentic workflows that keep open PRs healthy. Add a label to a PR → the agent checks it every 3 hours. diff --git a/docs/lsp.md b/docs/lsp.md index 4534ed476ad..9025068db68 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -1,3 +1,9 @@ +--- +title: F# LSP +category: Language Service Internals +categoryindex: 300 +index: 500 +--- # F# LSP F# LSP support design proposal. To be expanded as we learn more / settle on things. diff --git a/docs/optimizations-equality.md b/docs/optimizations-equality.md index ec8ee759eca..89d6a5558f3 100644 --- a/docs/optimizations-equality.md +++ b/docs/optimizations-equality.md @@ -1,3 +1,9 @@ +--- +title: Equality optimizations +category: Compiler Internals +categoryindex: 200 +index: 450 +--- # Compiling Equality This spec covers how equality is compiled and executed by the F# compiler and library, based mainly on the types involved in the equality operation after all inlining, type specialization and other optimizations have been applied. diff --git a/docs/perf-discussions-archive.md b/docs/perf-discussions-archive.md index 47f6b14bae9..f6a106d930a 100644 --- a/docs/perf-discussions-archive.md +++ b/docs/perf-discussions-archive.md @@ -1,3 +1,9 @@ +--- +title: Perf discussions archive +category: Compiler Internals +categoryindex: 200 +index: 970 +--- This is just a typed version of [these notes](https://github.com/dotnet/fsharp/issues/16498), generated during perf discussions on summer of 2023. Can be used as a reference point. --- diff --git a/docs/postmortems/README.md b/docs/postmortems/README.md index 0da4a4bd8e3..a0e12a23e74 100644 --- a/docs/postmortems/README.md +++ b/docs/postmortems/README.md @@ -1,8 +1,14 @@ +--- +title: Overview +category: Postmortems +categoryindex: 550 +index: 100 +--- # Postmortems Detailed write-ups of bugs that were hard to diagnose, had non-obvious root causes, or taught us something worth preserving. Each document captures the symptoms, root cause, fix, and timeline so that future contributors can recognize similar patterns early. -These are referenced from [agentic instructions](../../.github/instructions/) and serve as deeper reading — the instructions tell you *what* to do, the postmortems explain *why* the rules exist. +These are referenced from [agentic instructions](https://github.com/dotnet/fsharp/tree/main/.github/instructions) and serve as deeper reading — the instructions tell you *what* to do, the postmortems explain *why* the rules exist. ## Index diff --git a/docs/postmortems/regression-fs0229-bstream-misalignment.md b/docs/postmortems/regression-fs0229-bstream-misalignment.md index 0711b6feb0a..4a1a0e4bc0d 100644 --- a/docs/postmortems/regression-fs0229-bstream-misalignment.md +++ b/docs/postmortems/regression-fs0229-bstream-misalignment.md @@ -1,3 +1,9 @@ +--- +title: FS0229 B-stream misalignment +category: Postmortems +categoryindex: 550 +index: 200 +--- # Regression: FS0229 B-Stream Misalignment in TypedTreePickle ## Summary diff --git a/docs/postmortems/regression-legacy-inline-metadata-dynamic-invocation.md b/docs/postmortems/regression-legacy-inline-metadata-dynamic-invocation.md index 4c62d9ceb61..d5f76994c04 100644 --- a/docs/postmortems/regression-legacy-inline-metadata-dynamic-invocation.md +++ b/docs/postmortems/regression-legacy-inline-metadata-dynamic-invocation.md @@ -1,3 +1,9 @@ +--- +title: Legacy inline metadata dynamic invocation +category: Postmortems +categoryindex: 550 +index: 300 +--- # Regression: Legacy inline metadata decoded as non-inline, breaking cross-assembly SRTP ## Summary diff --git a/docs/postmortems/regression-parse-tree-fidelity-return-attributes.md b/docs/postmortems/regression-parse-tree-fidelity-return-attributes.md index fc5daa92179..ad375c1412b 100644 --- a/docs/postmortems/regression-parse-tree-fidelity-return-attributes.md +++ b/docs/postmortems/regression-parse-tree-fidelity-return-attributes.md @@ -1,3 +1,9 @@ +--- +title: Parse tree fidelity of return attributes +category: Postmortems +categoryindex: 550 +index: 500 +--- # Regression: `[]` Attributes Disappeared From the Untyped Syntax Tree ## Summary diff --git a/docs/postmortems/regression-sourcebuild-cpm-runtime-version-floor.md b/docs/postmortems/regression-sourcebuild-cpm-runtime-version-floor.md index 43b3eb8504d..5a9955f6115 100644 --- a/docs/postmortems/regression-sourcebuild-cpm-runtime-version-floor.md +++ b/docs/postmortems/regression-sourcebuild-cpm-runtime-version-floor.md @@ -1,3 +1,9 @@ +--- +title: Source-build CPM runtime version floor +category: Postmortems +categoryindex: 550 +index: 400 +--- # Regression: renamed CPM runtime-package pins broke VMR source-build ## Summary diff --git a/docs/reflectionfree-printing.md b/docs/reflectionfree-printing.md index 68e3091faf6..8a6746d18ce 100644 --- a/docs/reflectionfree-printing.md +++ b/docs/reflectionfree-printing.md @@ -1,3 +1,9 @@ +--- +title: Reflection-free printing +category: FSharp.Core +categoryindex: 500 +index: 200 +--- # Simple vs Reflection-based DU and Record printing This document describes two modes for printing Discriminated Unions (DUs) and Records in F#: a **simple** reflection-free mode that delegates to a `string`-like operator for printing field values, and a `sprintf` mode (`sprintf "%A"`), which uses **reflection** to create output looking like F# code. In this document, the terms *simple* and *reflection* are used to distinguish the two modes. diff --git a/docs/regression-testing-pipeline.md b/docs/regression-testing-pipeline.md index 7c0518f06ea..51c96095b8e 100644 --- a/docs/regression-testing-pipeline.md +++ b/docs/regression-testing-pipeline.md @@ -1,3 +1,9 @@ +--- +title: Regression testing pipeline +category: Compiler Internals +categoryindex: 200 +index: 950 +--- # F# Compiler Regression Testing This document describes the F# compiler regression testing functionality implemented as a reusable Azure DevOps template in `eng/templates/regression-test-jobs.yml` and integrated into the main PR pipeline (`azure-pipelines-PR.yml`). diff --git a/docs/release-notes/PENDING_BREAKING_CHANGES.md b/docs/release-notes/PENDING_BREAKING_CHANGES.md index 3ecca5bfacb..df80c994bea 100644 --- a/docs/release-notes/PENDING_BREAKING_CHANGES.md +++ b/docs/release-notes/PENDING_BREAKING_CHANGES.md @@ -1,3 +1,9 @@ +--- +title: Pending breaking changes +category: Release Notes +categoryindex: 600 +index: 5 +--- # Breaking Changes in Query Expression Fixes ## AnonymousObject Structural Equality 🔴 diff --git a/docs/running-documentation-locally.md b/docs/running-documentation-locally.md index ebecfd583b7..1fe09db1014 100644 --- a/docs/running-documentation-locally.md +++ b/docs/running-documentation-locally.md @@ -11,36 +11,48 @@ You can follow this guide to see the results of your document changes rendered i ## Setup -`fsharp/fsharp-compiler-docs` will clone the `dotnet/fsharp` repository first to generate the documentation. -You can however, easily run the documentation locally and modify the `docs` from `dotnet/fsharp`. +`fsharp/fsharp-compiler-docs` is driven by a [Fun.Build](https://github.com/slaveOftime/Fun.Build) script, `build.fsx`. +By default it clones `dotnet/fsharp` into a git-ignored `fsharp` folder, builds `FSharp.Compiler.Service` and +generates the site from that clone. To work on the `docs` of your own `dotnet/fsharp` checkout instead, point the +`FSHARP_REPO` environment variable at it: the clone is skipped and your checkout is built and read in place. -* Clone `fsharp/fsharp-compiler-docs` at the same level as your local `dotnet/fsharp` repository: +* Clone `fsharp/fsharp-compiler-docs`: git clone https://github.com/fsharp/fsharp-compiler-docs.git + cd fsharp-compiler-docs -* Restore the `FSharp.Compiler.Service` project in `fsharp-compiler-docs`: +* Run the `Build` pipeline once. It builds `FSharp.Compiler.Service.slnx` with the SDK your checkout pins (using + the `eng/common/dotnet.sh` bootstrap, so that SDK does not need to be on your `PATH`), restores the `fsdocs` tool + and generates the site into `output/`: - cd fsharp-compiler-docs/FSharp.Compiler.Service - dotnet restore + FSHARP_REPO=/path/to/your/fsharp dotnet fsi build.fsx -* Restore the local tools in `fsharp-compiler-docs`: +* Then serve the docs with live reload: - cd .. - dotnet tool restore + FSHARP_REPO=/path/to/your/fsharp dotnet fsi build.fsx -- -p Watch -* Run the documentation tool using your `dotnet/fsharp` fork as input. +Anything after the pipeline name is passed on to `fsdocs watch`, for example `-- -p Watch --nolaunch --port 8080`. +The `Watch` pipeline does not rebuild `FSharp.Compiler.Service`. Rerun `Build` after changing signature files or +XML documentation in `src/Compiler`. +## How watch works - dotnet fsdocs watch --eval --sourcefolder ../fsharp/ --input ../fsharp/docs/ +`fsdocs watch` is a lazy development server: a page is built the first time it is requested and cached until a +file that influences it changes. Diagnostics, including which `FSharp.Compiler.Service.dll` is being documented, +are at `/.fsdocs/doctor` on the served site. +A page is rebuilt when the content of its own `.md` or `.fsx` file changes. Files a script pulls in through +`#load`, and files it reads while evaluating, are not tracked +([FSharp.Formatting#1309](https://github.com/fsprojects/FSharp.Formatting/issues/1309)). This affects the release +notes pages in `docs/release-notes`, which are composed from the MarkDown files in the hidden subfolders through +`.aux/Common.fsx`: editing those does not regenerate the served page. Make any edit to the `.fsx` of the page +(touching the file is not enough, the content has to change), or restart the watch. -## Release notes caveat - -The release notes pages from `docs/release-notes` are composed from the MarkDown files in subfolders. -Changing any of these files, won't regenerate the served webpage. Only the changes to the `.fsx` will trigger the tool. This is a known limitation. +Restart the watch as well after changing the package versions referenced from `.aux/Common.fsx`. The running F# +Interactive session keeps the assemblies it already loaded. diff --git a/docs/srtp-guide.md b/docs/srtp-guide.md index 4165fb84673..eed6124b1a6 100644 --- a/docs/srtp-guide.md +++ b/docs/srtp-guide.md @@ -1,4 +1,8 @@ --- +title: SRTP guide (draft) +category: Compiler Internals +categoryindex: 200 +index: 980 status: draft target: Microsoft Learn (F# language guide) notes: >