I'm writing a recursive parser with FParsec, so I'm creating a dummy parser and reference cell with createParserForwardedToRef
like so:
let pstatement, pstatementref = createParserForwardedToRef<Statement, Unit>()
The reference cell is eventually "assigned" the following:
do pstatementref := choice [
pread
pdisplay
pset
pcompute
pif
pwhile
]
When I test the parser pstatement
from within the file in which the reference cell is created and assigned the above value with the following code ...
let test p str =
match runParserOnFile p () str Encoding.ASCII with
| Success(result, _, _) -> printfn "Success: %A" result
| Failure(errorMsg, _, _) -> printfn "Failure: %s" errorMsg
[<EntryPoint>]
let main argv =
argv
|> String.concat "+"
|> test (manyTill pstatement (pword "HALT"))
0
... it works well, and I get the successful results.
However, when I try to run the exact same test function, this time from another project (called Interpreter) that has referenced the project wherein pstatement
is defined (called Parser, after removing the main function from Program.fs in Parser), I receive the following error:
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at FParsec.Primitives.manyTill@890.Invoke(CharStream`1 stream)
at FParsec.CharParsers.applyParser[Result,UserState](FSharpFunc`2 parser, CharStream`1 stream)
at FParsec.CharParsers.runParserOnFile[a,u](FSharpFunc`2 parser, u ustate, String path, Encoding encoding)
at Program.test[a](FSharpFunc`2 p, String str) in /home/rosalogia/Projects/FSharp/PCParser/src/Interpreter/Program.fs:line 16
at Program.main(String[] argv) in /home/rosalogia/Projects/FSharp/PCParser/src/Interpreter/Program.fs:line 32
... which implies that the reference cell is never assigned any value, however the code which does so has not been removed from the Parser project.
Is there a limitation of reference cells I'm not aware of that's causing this problem? Since everything works fine within the file where the reference cell is defined, it seems safe to conclude that the problem is with accessing its value from a file or project outside of the one in which it's defined.
EDIT:
Here is a more complete example of how things are set up
Parser/Library.fs
namespace PCParser
module Parser =
let pstatement, pstatementref = createParserForwardedToRef<Statement, Unit>()
let pread = ...
let pdisplay = ...
...
do pstatementref := choice [
pread
pdisplay
pset
pcompute
pif
pwhile
]
Interpreter/Program.fs
open PCParser.Parser
open FParsec
open System.Text
let test p str =
match runParserOnFile p () str Encoding.ASCII with
| Success(result, _, _) -> printfn "Success: %A" result
| Failure(errorMsg, _, _) -> printfn "Failure: %s" errorMsg
[<EntryPoint>]
let main argv =
argv
|> String.concat "+"
|> test (manyTill pstatement (pword "HALT"))
0
My guess is that this is because the project you are referencing (which defines the parser) is compiled as an executable rather than as a library.
Here is a minimal example that exhibits similar behaviour. Say we have project1
with the following:
module Xyz
let r = ref None
do r := Some 42
And we have project2
that references project1
and has the following code:
printfn "GOT: %A" Xyz.r
If you compile project1
as an executable, running the code will print GOT: <null>
, but if you change output type of project1
to be a class library (dll) then this code will print GOT: { contents=Some 42 }
.
The reason is that the F# compiler compiles initialization code differently for class libraries and executables. For executables, it assumes that the executable will be executed and so it runs initialization code in the main
function. For libraries, it cannot assume that and so it puts checks in static constructors.
To fix this, you either need to compile your code as a class library, or you can add some custom initialization function that the test project can call before running tests (and which can be called from main
)