Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pull-requests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ jobs:
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
sarif_file: ./analysis.sarif
category: fsharp-analyzers
1 change: 1 addition & 0 deletions .github/workflows/push-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Changelog

## [Unreleased]
## [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
Expand Down
6 changes: 3 additions & 3 deletions build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,11 @@ let generatedSources =
"src/FsYacc.Core/fsyaccpars.fsi"
]

/// 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/JsonLexAndYaccExample/Lexer.fs"
"tests/JsonLexAndYaccExample/Parser.fs"
"tests/JsonLexAndYaccExample/Parser.fsi"
"tests/LexAndYaccMiniProject/Lexer.fs"
"tests/LexAndYaccMiniProject/Parser.fs"
"tests/LexAndYaccMiniProject/Parser.fsi"
Expand Down
4 changes: 2 additions & 2 deletions docs/content/fslex.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Or you can add it to your build project via entries like this:
<FsLex Include="..\LexAndYaccMiniProject\Lexer.fsl">
<OtherFlags>--module Lexer --unicode</OtherFlags>
</FsLex>
<Import Project="FsLexYacc.Build.Tasks\FsLexYacc.targets" />

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
Expand Down
24 changes: 24 additions & 0 deletions docs/content/fsyacc.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,30 @@ But you must manually add `FsLex` andd `FsYacc` entries inside of an `ItemGroup`
<OtherFlags>--unicode</OtherFlags>
</FsLex>

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`:

<ItemGroup>
<FsYacc Include="Parser.fsy">
<OtherFlags>--module Parser</OtherFlags>
</FsYacc>
<FsLex Include="Lexer.fsl">
<OtherFlags>--unicode</OtherFlags>
</FsLex>
<Compile Include="$(FsYaccOutputFolder)Parser.fsi" />
<Compile Include="$(FsYaccOutputFolder)Parser.fs" />
<Compile Include="$(FsLexOutputFolder)Lexer.fs" />
</ItemGroup>

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`:

<PropertyGroup>
<FsLexYaccOutputToIntermediate>false</FsLexYaccOutputToIntermediate>
</PropertyGroup>

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:

<FsYacc Include="..\LexAndYaccMiniProject\Parser.fsy">
Expand Down
4 changes: 2 additions & 2 deletions docs/content/jsonParserExample.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<Compile Include="$(FsYaccOutputFolder)Parser.fs" />``.

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 ``%}``.

Expand Down Expand Up @@ -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 ``<Compile Include="$(FsLexOutputFolder)Lexer.fs" />``.

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 <em>Parser</em> module which contains the union type with the tokens. We will use them in our productions.

Expand Down
53 changes: 51 additions & 2 deletions src/FsLexYacc.Build.Tasks/FsLexYacc.targets
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,61 @@ Copyright (C) Microsoft Corporation. All rights reserved.
<FsLexYaccToolRunner Condition="'$(UseFsLexYaccToolRunner)' != 'false' AND '$(FsLexYaccToolRunner)' == ''">dotnet </FsLexYaccToolRunner>
</PropertyGroup>

<!--
The generated .fs and .fsi go under $(IntermediateOutputPath) by default, so they stay out of the source
tree and `dotnet clean` removes them (MSBuild only honours FileWrites under the output or intermediate
folder). Projects reference the generated code as $(FsLexOutputFolder)Lexer.fs and
$(FsYaccOutputFolder)Parser.fs(i). FsLexYaccOutputToIntermediate=false writes them next to the grammar
instead, as FsLexYacc 12 and earlier did.

The defaults are resolved here, at evaluation time, so that the properties can be used in Compile items.
That requires IntermediateOutputPath to be set already, which is the case when this file is imported
through the NuGet package (after the SDK targets). A project that imports this file directly from its
body evaluates it before the SDK targets, where IntermediateOutputPath is still empty; the Compile items
would then silently point next to the grammar, so FsLexYaccCheckOutputFolders fails the build instead.
-->
<PropertyGroup>
<FsLexYaccOutputToIntermediate Condition="'$(FsLexYaccOutputToIntermediate)' == ''">true</FsLexYaccOutputToIntermediate>
</PropertyGroup>

<PropertyGroup Condition="'$(FsLexYaccOutputToIntermediate)' == 'true' AND '$(IntermediateOutputPath)' != ''">
<FsLexOutputFolder Condition="'$(FsLexOutputFolder)' == ''">$(IntermediateOutputPath)</FsLexOutputFolder>
<FsYaccOutputFolder Condition="'$(FsYaccOutputFolder)' == ''">$(IntermediateOutputPath)</FsYaccOutputFolder>
</PropertyGroup>

<Target Name="FsLexYaccCheckOutputFolders">
<Error
Condition="'$(FsLexYaccOutputToIntermediate)' == 'true' AND ('$(FsLexOutputFolder)' == '' OR '$(FsYaccOutputFolder)' == '')"
Text="FsLexYacc writes the generated lexer and parser to the intermediate folder, but FsLexYacc.targets was imported before IntermediateOutputPath was defined, so FsLexOutputFolder and FsYaccOutputFolder could not be resolved. Import FsLexYacc.targets after the SDK targets (for example through the FsLexYacc NuGet package, or after an explicit &lt;Import Project=&quot;Sdk.targets&quot; Sdk=&quot;Microsoft.NET.Sdk&quot; /&gt;), set FsLexOutputFolder and FsYaccOutputFolder yourself, or set FsLexYaccOutputToIntermediate to false to write them next to the grammar." />

<!--
A project written for the old default compiles Parser.fs next to Parser.fsy. After the upgrade that file
is no longer regenerated, so the build would keep compiling a stale copy. Fail on any Compile item that
points next to a grammar when the generated file is written somewhere else.
-->
<ItemGroup>
<_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)" />
</ItemGroup>
<Error
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 &lt;Compile Include=&quot;%24(FsYaccOutputFolder)Parser.fs&quot; /&gt;, and delete the stale copies. Set FsLexYaccOutputToIntermediate to false to keep writing them next to the grammar." />
</Target>

<!-- Build FsLex files. -->
<Target
Name="CallFsLex"
Inputs="@(FsLex)"
Outputs="@(FsLex->'$(FsLexOutputFolder)%(Filename).fs');@(FsLex->'$(FsLexOutputFolder)%(Filename).fsi')"
Condition="'@(FsLex)'!=''"
BeforeTargets="CoreCompile">
DependsOnTargets="FsLexYaccCheckOutputFolders"
BeforeTargets="CoreCompile">

<!-- Create the output directory -->
<MakeDir Directories="$(FsLexOutputFolder)"/>
Expand All @@ -46,7 +94,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">

<!-- Create the output directory -->
<MakeDir Directories="$(FsYaccOutputFolder)"/>
Expand Down
4 changes: 0 additions & 4 deletions tests/JsonLexAndYaccExample/.gitignore

This file was deleted.

20 changes: 14 additions & 6 deletions tests/JsonLexAndYaccExample/JsonLexAndYaccExample.fsproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<?xml version="1.0" encoding="utf-8"?>
<!--
Imports the SDK explicitly so that FsLexYacc.targets is evaluated after it, the way a project
that gets the targets from the NuGet package sees them. That is what lets the Compile items
below use $(FsLexOutputFolder) and $(FsYaccOutputFolder), which point at the intermediate folder
by default. LexAndYaccMiniProject covers FsLexYaccOutputToIntermediate=false, the in-tree output.
-->
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
Expand All @@ -14,8 +21,8 @@
<FsLex Include="Lexer.fsl">
<OtherFlags>--unicode</OtherFlags>
</FsLex>
<Compile Include="Parser.fs" />
<Compile Include="Lexer.fs" />
<Compile Include="$(FsYaccOutputFolder)Parser.fs" />
<Compile Include="$(FsLexOutputFolder)Lexer.fs" />
<Compile Include="Program.fs" />
<None Include="Script.fsx" />
<None Include="randomComplexTestsJson.json">
Expand All @@ -25,5 +32,6 @@
<ItemGroup>
<ProjectReference Include="..\..\src\FsLexYacc.Runtime\FsLexYacc.Runtime.fsproj" />
</ItemGroup>
<Import Project="..\..\src\FsLexYacc.Build.Tasks\FsLexYacc.targets" />
</Project>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
<Import Project="..\..\src\FsLexYacc.Build.Tasks\FsLexYacc.targets" />
</Project>
2 changes: 2 additions & 0 deletions tests/LexAndYaccMiniProject/LexAndYaccMiniProject.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<TargetFramework>net10.0</TargetFramework>
<FsLexToolPath>..\..\src\FsLex\bin\$(Configuration)\net10.0</FsLexToolPath>
<FsYaccToolPath>..\..\src\FsYacc\bin\$(Configuration)\net10.0</FsYaccToolPath>
<!-- Covers the opt-out: the generated sources are written next to the grammar. -->
<FsLexYaccOutputToIntermediate>false</FsLexYaccOutputToIntermediate>
</PropertyGroup>
<ItemGroup>
<FsYacc Include="Parser.fsy">
Expand Down
Loading