From 80962b95afe0b15c57c5c6d7899771aac9bb8a49 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Wed, 16 Sep 2026 15:12:30 +0200 Subject: [PATCH 1/4] Add an opt-in to write the generated lexer and parser to obj MSBuild only honours FileWrites under the output or intermediate folder, so the registration that was meant to let `dotnet clean` remove the generated .fs and .fsi never took effect: by default they land next to the grammar, outside both folders. FsLexYaccOutputToIntermediate=true now defaults FsLexOutputFolder and FsYaccOutputFolder to IntermediateOutputPath, so clean removes the generated sources and they stay out of the source tree. Projects reference them as $(FsLexOutputFolder)Lexer.fs and $(FsYaccOutputFolder)Parser.fs(i). Because every existing project references Parser.fs next to Parser.fsy, this is opt-in for now and becomes the default in the next major version. The defaults are resolved at evaluation time so the properties can be used in Compile items, which requires the targets to be imported after the SDK targets, as the NuGet package does. A project that imports the targets file from its body would otherwise get Compile items that silently point next to the grammar, so FsLexYaccCheckOutputFolders fails the build with an error that explains the cause and the fix. JsonLexAndYaccExample opts in to cover the new mode, and imports the SDK explicitly so the targets evaluate the way a package consumer sees them. LexAndYaccMiniProject keeps the default so both modes stay covered. Release as 12.2.0. Closes #247 --- CHANGELOG.md | 5 +++- build.fsx | 5 ++-- docs/content/fsyacc.md | 19 ++++++++++++ src/FsLexYacc.Build.Tasks/FsLexYacc.targets | 29 +++++++++++++++++-- tests/JsonLexAndYaccExample/.gitignore | 4 --- .../JsonLexAndYaccExample.fsproj | 21 ++++++++++---- 6 files changed, 67 insertions(+), 16 deletions(-) delete mode 100644 tests/JsonLexAndYaccExample/.gitignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b354f5..5bdb65b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog -## [Unreleased] +## [12.2.0] - 2026-09-16 + +### Added +* `FsLexYaccOutputToIntermediate` writes the generated lexer and parser to the intermediate folder instead of next to the grammar, so `dotnet clean` removes them. Reference the generated files as `$(FsLexOutputFolder)Lexer.fs` and `$(FsYaccOutputFolder)Parser.fs(i)`. This is opt-in for now and becomes the default in the next major version. [#247](https://github.com/fsprojects/FsLexYacc/issues/247) ### Changed * Lower the required `FSharp.Core` from `10.1.400` to `10.0.100`. [#253](https://github.com/fsprojects/FsLexYacc/pull/253) diff --git a/build.fsx b/build.fsx index d30dfb7..3efd3fd 100755 --- a/build.fsx +++ b/build.fsx @@ -141,11 +141,10 @@ let generatedSources = "src/FsYacc.Core/fsyaccpars.fsi" ] +/// JsonLexAndYaccExample is not listed: it opts into FsLexYaccOutputToIntermediate, so its +/// generated sources live under obj and go away with `dotnet clean`. let generatedTestSources = [ - "tests/JsonLexAndYaccExample/Lexer.fs" - "tests/JsonLexAndYaccExample/Parser.fs" - "tests/JsonLexAndYaccExample/Parser.fsi" "tests/LexAndYaccMiniProject/Lexer.fs" "tests/LexAndYaccMiniProject/Parser.fs" "tests/LexAndYaccMiniProject/Parser.fsi" diff --git a/docs/content/fsyacc.md b/docs/content/fsyacc.md index 3a56673..029beca 100644 --- a/docs/content/fsyacc.md +++ b/docs/content/fsyacc.md @@ -93,6 +93,25 @@ But you must manually add `FsLex` andd `FsYacc` entries inside of an `ItemGroup` --unicode +By default the generated `.fs` and `.fsi` files are written next to the grammar, and `dotnet clean` leaves them in place. Set `FsLexYaccOutputToIntermediate` to write them to the intermediate folder (`obj/...`) instead, where `dotnet clean` removes them and they stay out of your source tree. Reference the generated files through `FsLexOutputFolder` and `FsYaccOutputFolder`: + + + true + + + + --module Parser + + + --unicode + + + + + + +Setting `FsLexOutputFolder` or `FsYaccOutputFolder` yourself still takes precedence. The option relies on `FsLexYacc.targets` being imported after the SDK targets, which is how the NuGet package imports it. If you import the targets file yourself from the project body, the build fails with an error telling you so. A future major version will make the intermediate folder the default. + When the grammar has shift/reduce or reduce/reduce conflicts, `FsYacc` prints only how many there are. To see each conflict, with the state, the terminal and the two actions involved, add `-v` in the `OtherFlags` section. That writes a `.fsyacc.output` listing file next to the generated parser containing the conflicts and the LALR tables: diff --git a/src/FsLexYacc.Build.Tasks/FsLexYacc.targets b/src/FsLexYacc.Build.Tasks/FsLexYacc.targets index 2d825dd..49a45ea 100644 --- a/src/FsLexYacc.Build.Tasks/FsLexYacc.targets +++ b/src/FsLexYacc.Build.Tasks/FsLexYacc.targets @@ -22,13 +22,37 @@ Copyright (C) Microsoft Corporation. All rights reserved. dotnet + + + $(IntermediateOutputPath) + $(IntermediateOutputPath) + + + + + + + DependsOnTargets="FsLexYaccCheckOutputFolders" + BeforeTargets="CoreCompile"> @@ -46,7 +70,8 @@ Copyright (C) Microsoft Corporation. All rights reserved. Inputs="@(FsYacc)" Outputs="@(FsYacc->'$(FsYaccOutputFolder)%(Filename).fs');@(FsYacc->'$(FsYaccOutputFolder)%(Filename).fsi')" Condition="'@(FsYacc)'!=''" - BeforeTargets="CoreCompile"> + DependsOnTargets="FsLexYaccCheckOutputFolders" + BeforeTargets="CoreCompile"> diff --git a/tests/JsonLexAndYaccExample/.gitignore b/tests/JsonLexAndYaccExample/.gitignore deleted file mode 100644 index 12bea09..0000000 --- a/tests/JsonLexAndYaccExample/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -Lexer.fs -Lexer.fsi -Parser.fs -Parser.fsi diff --git a/tests/JsonLexAndYaccExample/JsonLexAndYaccExample.fsproj b/tests/JsonLexAndYaccExample/JsonLexAndYaccExample.fsproj index 030b188..5c01002 100644 --- a/tests/JsonLexAndYaccExample/JsonLexAndYaccExample.fsproj +++ b/tests/JsonLexAndYaccExample/JsonLexAndYaccExample.fsproj @@ -1,10 +1,18 @@ - - + + + + Exe net10.0 ..\..\src\FsLex\bin\$(Configuration)\net10.0 ..\..\src\FsYacc\bin\$(Configuration)\net10.0 + true @@ -14,8 +22,8 @@ --unicode - - + + @@ -25,5 +33,6 @@ - - \ No newline at end of file + + + From 76e3c02e44a4ff9ea8b3c16a84b82f27e2a48d7e Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Wed, 16 Sep 2026 15:25:59 +0200 Subject: [PATCH 2/4] Add missing category --- .github/workflows/pull-requests.yml | 1 + .github/workflows/push-main.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 0ab560f..6c3da2d 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -47,3 +47,4 @@ jobs: uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: sarif_file: ./analysis.sarif + category: fsharp-analyzers diff --git a/.github/workflows/push-main.yml b/.github/workflows/push-main.yml index 182252d..fbc6695 100644 --- a/.github/workflows/push-main.yml +++ b/.github/workflows/push-main.yml @@ -52,6 +52,7 @@ jobs: uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: sarif_file: ./analysis.sarif + category: fsharp-analyzers - name: Build documentation run: dotnet fsi build.fsx -p Docs - name: Upload documentation From 6873b7ce9dff6aa6bed2d7c975aed7d43a4c3344 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Thu, 24 Sep 2026 22:17:59 +0200 Subject: [PATCH 3/4] Write the generated lexer and parser to obj by default MSBuild targets are expected to stay out of the source tree, since writing there invites circularity and subtle build issues. FsLexOutputFolder and FsYaccOutputFolder now default to IntermediateOutputPath, so the generated .fs and .fsi stay out of the source tree and `dotnet clean` removes them. FsLexYaccOutputToIntermediate=false keeps the previous behaviour of writing them next to the grammar. A project written for the old default compiles Parser.fs next to Parser.fsy. After the upgrade that copy is no longer regenerated, so the build would silently keep compiling a stale file. FsLexYaccCheckOutputFolders now fails the build in that case and explains how to reference the generated files or opt out. Its existing error for importing the targets before the SDK now also points at the opt-out. JsonLexAndYaccExample covers the new default and LexAndYaccMiniProject opts out, so both modes stay covered. Release as 13.0.0. --- CHANGELOG.md | 6 +-- build.fsx | 5 ++- docs/content/fslex.md | 4 +- docs/content/fsyacc.md | 15 +++++--- docs/content/jsonParserExample.md | 4 +- src/FsLexYacc.Build.Tasks/FsLexYacc.targets | 38 +++++++++++++++---- .../JsonLexAndYaccExample.fsproj | 5 +-- .../LexAndYaccMiniProject.fsproj | 2 + 8 files changed, 54 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bdb65b..691d620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,9 @@ # Changelog -## [12.2.0] - 2026-09-16 - -### Added -* `FsLexYaccOutputToIntermediate` writes the generated lexer and parser to the intermediate folder instead of next to the grammar, so `dotnet clean` removes them. Reference the generated files as `$(FsLexOutputFolder)Lexer.fs` and `$(FsYaccOutputFolder)Parser.fs(i)`. This is opt-in for now and becomes the default in the next major version. [#247](https://github.com/fsprojects/FsLexYacc/issues/247) +## [13.0.0] - 2026-09-24 ### Changed +* **Breaking:** the generated lexer and parser are written to the intermediate folder (`obj/...`) instead of next to the grammar, so they stay out of the source tree and `dotnet clean` removes them. Reference the generated files as `$(FsLexOutputFolder)Lexer.fs` and `$(FsYaccOutputFolder)Parser.fs(i)`. The build fails with an explanation when a project still compiles `Parser.fs` or `Lexer.fs` next to the grammar. Set `FsLexYaccOutputToIntermediate` to `false` to keep the old location. [#247](https://github.com/fsprojects/FsLexYacc/issues/247) * Lower the required `FSharp.Core` from `10.1.400` to `10.0.100`. [#253](https://github.com/fsprojects/FsLexYacc/pull/253) ### Fixed diff --git a/build.fsx b/build.fsx index 3efd3fd..d38da67 100755 --- a/build.fsx +++ b/build.fsx @@ -141,8 +141,9 @@ let generatedSources = "src/FsYacc.Core/fsyaccpars.fsi" ] -/// JsonLexAndYaccExample is not listed: it opts into FsLexYaccOutputToIntermediate, so its -/// generated sources live under obj and go away with `dotnet clean`. +/// JsonLexAndYaccExample is not listed: it uses the default output to the intermediate folder, so +/// its generated sources live under obj and go away with `dotnet clean`. LexAndYaccMiniProject +/// opts out with FsLexYaccOutputToIntermediate=false. let generatedTestSources = [ "tests/LexAndYaccMiniProject/Lexer.fs" diff --git a/docs/content/fslex.md b/docs/content/fslex.md index e326235..51ce415 100644 --- a/docs/content/fslex.md +++ b/docs/content/fslex.md @@ -22,8 +22,8 @@ Or you can add it to your build project via entries like this: --module Lexer --unicode - - + +The generated files are written to the intermediate folder (`obj/...`). See [MSBuild support](fsyacc.html) for how to reference them, or how to write them next to the grammar instead. Lexer syntax diff --git a/docs/content/fsyacc.md b/docs/content/fsyacc.md index 029beca..fee414f 100644 --- a/docs/content/fsyacc.md +++ b/docs/content/fsyacc.md @@ -93,11 +93,8 @@ But you must manually add `FsLex` andd `FsYacc` entries inside of an `ItemGroup` --unicode -By default the generated `.fs` and `.fsi` files are written next to the grammar, and `dotnet clean` leaves them in place. Set `FsLexYaccOutputToIntermediate` to write them to the intermediate folder (`obj/...`) instead, where `dotnet clean` removes them and they stay out of your source tree. Reference the generated files through `FsLexOutputFolder` and `FsYaccOutputFolder`: +The generated `.fs` and `.fsi` files are written to the intermediate folder (`obj/...`), so they stay out of your source tree and `dotnet clean` removes them. Reference them through `FsLexOutputFolder` and `FsYaccOutputFolder`: - - true - --module Parser @@ -110,7 +107,15 @@ By default the generated `.fs` and `.fsi` files are written next to the grammar, -Setting `FsLexOutputFolder` or `FsYaccOutputFolder` yourself still takes precedence. The option relies on `FsLexYacc.targets` being imported after the SDK targets, which is how the NuGet package imports it. If you import the targets file yourself from the project body, the build fails with an error telling you so. A future major version will make the intermediate folder the default. +Setting `FsLexOutputFolder` or `FsYaccOutputFolder` yourself takes precedence. The default relies on `FsLexYacc.targets` being imported after the SDK targets, which is how the NuGet package imports it. If you import the targets file yourself from the project body, the build fails with an error telling you so. + +To write the generated files next to the grammar instead, as FsLexYacc 12 and earlier did, set `FsLexYaccOutputToIntermediate` to `false`: + + + false + + +When upgrading from FsLexYacc 12, a project that still compiles `Parser.fs` or `Lexer.fs` next to the grammar fails the build, because those copies are no longer regenerated. Change the `Compile` items as shown above and delete the old generated files, or opt out. When the grammar has shift/reduce or reduce/reduce conflicts, `FsYacc` prints only how many there are. To see each conflict, with the state, the terminal and the two actions involved, add `-v` in the `OtherFlags` section. That writes a `.fsyacc.output` listing file next to the generated parser containing the conflicts and the LALR tables: diff --git a/docs/content/jsonParserExample.md b/docs/content/jsonParserExample.md index 37a6bf0..8ebfb9e 100644 --- a/docs/content/jsonParserExample.md +++ b/docs/content/jsonParserExample.md @@ -114,7 +114,7 @@ Reload/Open the project and add the following code to ``Parser.fsy``: | value { [$1] } | rev_values COMMA value { $3 :: $1 } -This file is describing parsing rules and tokens. When you build the project, a new file called ``Parser.fs`` will be created in the project root directory. Include it in your project. +This file is describing parsing rules and tokens. When you build the project, a new file called ``Parser.fs`` will be created in the intermediate folder (``obj/...``). Include it in your project with ````. Lets take a closer look at the parser definition (``Parser.fsy)``. At the very top of the file there is a section for ``open`` statements. You can open any namespace or module you want. We need only ``JsonParsing`` where we defined our ``JsonValue`` structure. All open statements should be between `` %{`` and ``%}``. @@ -215,7 +215,7 @@ Reload/Open the project and add the following code to ``Lexer.fsl:`` | [^ '"' '\\']+ { read_string (str + (lexeme lexbuf)) false lexbuf } | eof { raise (Exception ("String is not terminated")) } -When you build the project, a new file called ``Lexer.fs`` will be created in the project root directory. Include it in your project after ``Parser.fs``. +When you build the project, a new file called ``Lexer.fs`` will be created in the intermediate folder. Include it in your project after ``Parser.fs`` with ````. The first part of a lexer is simply F# code enclosed in ``{}``. It defines a module, opens namespaces and defines helper functions. The ``lexeme`` function will extract the matched string from the buffer. The ``newline`` function updates the buffer position to skip new line characters. Notice that we open the Parser module which contains the union type with the tokens. We will use them in our productions. diff --git a/src/FsLexYacc.Build.Tasks/FsLexYacc.targets b/src/FsLexYacc.Build.Tasks/FsLexYacc.targets index 49a45ea..9750d21 100644 --- a/src/FsLexYacc.Build.Tasks/FsLexYacc.targets +++ b/src/FsLexYacc.Build.Tasks/FsLexYacc.targets @@ -23,10 +23,11 @@ Copyright (C) Microsoft Corporation. All rights reserved. + + true + + $(IntermediateOutputPath) $(IntermediateOutputPath) - + + + + + + <_FsLexYaccNextToGrammar Include="@(FsLex->'%(RootDir)%(Directory)%(Filename).fs');@(FsLex->'%(RootDir)%(Directory)%(Filename).fsi')" /> + <_FsLexYaccNextToGrammar Include="@(FsYacc->'%(RootDir)%(Directory)%(Filename).fs');@(FsYacc->'%(RootDir)%(Directory)%(Filename).fsi')" /> + <_FsLexYaccGenerated Include="@(FsLex->'$(FsLexOutputFolder)%(Filename).fs');@(FsLex->'$(FsLexOutputFolder)%(Filename).fsi')" /> + <_FsLexYaccGenerated Include="@(FsYacc->'$(FsYaccOutputFolder)%(Filename).fs');@(FsYacc->'$(FsYaccOutputFolder)%(Filename).fsi')" /> + <_FsLexYaccNextToGrammar Remove="@(_FsLexYaccGenerated->'%(FullPath)')" /> + <_FsLexYaccCompiled Include="@(Compile->'%(FullPath)')" /> + <_FsLexYaccNotCompiled Include="@(_FsLexYaccNextToGrammar)" Exclude="@(_FsLexYaccCompiled)" /> + <_FsLexYaccStaleCompile Include="@(_FsLexYaccNextToGrammar)" Exclude="@(_FsLexYaccNotCompiled)" /> + + Condition="'@(_FsLexYaccStaleCompile)' != ''" + Text="The project compiles @(_FsLexYaccStaleCompile->'%(Filename)%(Extension)', ', ') next to the grammar, but FsLexYacc writes the generated files to $(FsLexOutputFolder) (FsLex) and $(FsYaccOutputFolder) (FsYacc). Reference them through the FsLexOutputFolder and FsYaccOutputFolder properties, for example <Compile Include="%24(FsYaccOutputFolder)Parser.fs" />, and delete the stale copies. Set FsLexYaccOutputToIntermediate to false to keep writing them next to the grammar." /> diff --git a/tests/JsonLexAndYaccExample/JsonLexAndYaccExample.fsproj b/tests/JsonLexAndYaccExample/JsonLexAndYaccExample.fsproj index 5c01002..3c0a1bd 100644 --- a/tests/JsonLexAndYaccExample/JsonLexAndYaccExample.fsproj +++ b/tests/JsonLexAndYaccExample/JsonLexAndYaccExample.fsproj @@ -2,8 +2,8 @@ @@ -12,7 +12,6 @@ net10.0 ..\..\src\FsLex\bin\$(Configuration)\net10.0 ..\..\src\FsYacc\bin\$(Configuration)\net10.0 - true diff --git a/tests/LexAndYaccMiniProject/LexAndYaccMiniProject.fsproj b/tests/LexAndYaccMiniProject/LexAndYaccMiniProject.fsproj index c3db814..3c9c6dd 100644 --- a/tests/LexAndYaccMiniProject/LexAndYaccMiniProject.fsproj +++ b/tests/LexAndYaccMiniProject/LexAndYaccMiniProject.fsproj @@ -5,6 +5,8 @@ net10.0 ..\..\src\FsLex\bin\$(Configuration)\net10.0 ..\..\src\FsYacc\bin\$(Configuration)\net10.0 + + false From 40eff5b4f774ce332ad8ce486e83154240106f8a Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Thu, 24 Sep 2026 22:19:33 +0200 Subject: [PATCH 4/4] Bump tools --- .config/dotnet-tools.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index e1ecfc8..bec6d2d 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,14 +3,14 @@ "isRoot": true, "tools": { "fantomas": { - "version": "8.0.0", + "version": "8.0.4", "commands": [ "fantomas" ], "rollForward": false }, "fsdocs-tool": { - "version": "23.0.0-alpha.4", + "version": "23.0.0-alpha.8", "commands": [ "fsdocs" ],