Search code examples
functional-programmingf#fparsec

F# - Accessing Reference Cells from Different Projects


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 [email protected](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

Solution

  • 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)