3

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
Ambika E
  • 93
  • 5
  • Where exactly is the call that assigns the reference? How are you executing it? – Fyodor Soikin Sep 22 '20 at 07:19
  • The call that assigns the reference is in the Parser project and module, which I `open` from the Interpreter project. My assumption was that the assignment would be executed by default when the module was opened. – Ambika E Sep 22 '20 at 13:00
  • Can you post a minimal repro please? It's not clear from your explanation what is contained/nested where. – Fyodor Soikin Sep 22 '20 at 13:08
  • I've tried to clarify in an edit how things are set up. Tomas's minimal example works as a minimal reproduction of my issue, however I'm still unsure how it can be solved. I changed the Parser project to a classlib only recently, but the issue persists. – Ambika E Sep 22 '20 at 13:45

1 Answers1

4

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)

Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • This is helpful information and I definitely overlooked this. How do we go about ensuring that a project is compiled as a classlib besides initialising it with `dotnet new classlib -lang F# ...`? I've actually tried recreating the Parser project that way with no luck, so maybe I'm missing something. – Ambika E Sep 22 '20 at 12:59
  • 1
    I think `dotnet new` generates a `.fsproj` file - in this file, you can set `` to `Exe` (for executable) or `Library` (for a library) – Tomas Petricek Sep 22 '20 at 22:42