[GHC] #13801: Make -main-is work with {thing} from arbitrary installed packages

#13801: Make -main-is work with {thing} from arbitrary installed packages -------------------------------------+------------------------------------- Reporter: SimonHengel | Owner: (none) Type: feature | Status: new request | Priority: normal | Milestone: Component: Compiler | Version: 8.0.2 Keywords: | Operating System: Unknown/Multiple Architecture: | Type of failure: None/Unknown Unknown/Multiple | Test Case: | Blocked By: Blocking: | Related Tickets: Differential Rev(s): | Wiki Page: -------------------------------------+------------------------------------- = TL;DR Conceptually, `-main-is {thing}` is useful when writing unit tests. It allows you to test the code in your "Main" module by allowing you to use a different name for it. But using it that way will always result in double compilation (that is, all your code has to be compiled twice, once for your executable and once for your test suite). The reason for this is that `{thing}` has to be part of the currently compiled package (aka the `main` package). As a consequent, when using `-is-main`, it is not possible to define a library that is used by both the executable and the test suite. I propose that `-main-is` is extended so that `{thing}` can be from any ''installed package''. I'll give a somewhat detailed motivation below. Please feel free to fast- forward to the last section, which gives a test case (or rather acceptance criteria) for this feature request. = The full story == How to unit test code without the use of {{{-main-is}}} This section shows a common way to structure code for an executable, so that it is possible to: 1. write unit tests for all the code 1. avoid double compilation between the executable and the test suite (for the reminder of this text I call these two properties ''desirable properties'') === Code as a library Define all your code outside of `Main`, including your `main` function. As an example, let's assume we define our code in `src/My/Awesome/Tool.hs`, which defines the module `My.Awesome.Tool` and a "main" function named `run` in it: {{{#!hs -- src/My/Awesome/Tool.hs module My.Awesome.Tool where run :: IO () run = do ... }}} === Tests that use the library It is then possible to write tests for that code by importing the library module, e.g.: {{{#!hs -- test/Main.hs module Main where imports My.Awesome.Tool main = do -- unit tests go here ... }}} === Executable as a thin wrapper around the library code To compile an actual executable we create a ''driver''. The driver imports the library module and defines a `main` function. For our example this would looks something like this: {{{#!hs -- driver/Main.hs module Main where import My.Awesome.Tool (run) main = run }}} '''Note:''' The driver does not define any non-trivial code. This is to retain our first desirable property. === Compiling everything with Cabal It is then possible to compile everything with Cabal, using a Cabal file similar to this one: {{{ -- my-awesome-tool.cabal name: my-awesome-tool library hs-source-dirs: src exposed-modules: My.Awesome.Tool test-suite test type: exitcode-stdio-1.0 build-depends: my-awesome-tool hs-source-dirs: test main-is: Main.hs executable my-awesome-tool build-depends: my-awesome-tool hs-source-dirs: driver main-is: Main.hs }}} '''Note:''' Both, the executable and the test suite depend on the library component. This avoids double compilation, one of our desirable properties. == Removing the need for a driver by using `-main-is` It is possible to get rid of the need for a driver by using `-main-is`: {{{ -- my-awesome-tool.cabal ... executable my-awesome-tool hs-source-dirs: src main-is: My/Awesome/Tool.hs ghc-options: -main-is My.Awesome.Tool.run }}} But doing so results in double compilation: The executable can no longer depend on the library component. This is expected behavior, as stated in the documentation:
Strictly speaking, `-main-is` is not a link-phase flag at all; it has no effect on the link step. The flag must be specified when compiling the module containing the specified main function
== Shortcomings of `-main-is` According to the documentation, the purpose of `-main-is` is:
When testing, it is often convenient to change which function is the “main” one, and the `-main-is` flag allows you to do so.
It is not very explicit what "when testing" refers to here, but for the lack of any other evidence I assume this refers to unit testing. As far as I can tell, there is no way to use `-main-is` for unit testing without double compilation. '''Or in other words:''' If we use `-main-is` for it's stated purpose we always loose the second of our desirable properties. Please correct me if you think that I'm wrong. == How is `-main-is` implemented? I haven't looked at any code, but my assumption is that GHC generates a driver module, similar to the one we have to write by hand if we don't use `-main-is`. Can somebody confirm (or negate) this? == Proposed change I propose that GHC always generates the driver when `-main-is {thing}` is specified. GHC should even generate the driver if `{thing}` is not part of the currently compiled package (specifically `{thing}` is defined in an ''installed package'', not the `main` package). == (manual) test case This test case uses my `hpack` package (but any package that defines some function of type `IO ()` should work): {{{ $ cabal install hpack $ ghc -package hpack -main-is Hpack.main -o hpack }}} === expected result An executable named `hpack` is compiled that uses `Hpack.main` from the installed package `hpack` as entry point. === actual result {{{ ghc: no input files Usage: For basic information, try the `--help' option. }}} -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/13801 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#13801: Make -main-is work with {thing} from arbitrary installed packages -------------------------------------+------------------------------------- Reporter: SimonHengel | Owner: (none) Type: feature request | Status: new Priority: normal | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by SimonHengel): A related `hpack` issue: https://github.com/sol/hpack/issues/173#issue-229003256 -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/13801#comment:1 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#13801: Make -main-is work with {thing} from arbitrary installed packages -------------------------------------+------------------------------------- Reporter: SimonHengel | Owner: (none) Type: feature request | Status: new Priority: normal | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Changes (by bgamari): * cc: ezyang (added) Comment: CCing ezyang due to his interest in package system concerns.
I haven't looked at any code, but my assumption is that GHC generates a driver module, similar to the one we have to write by hand if we don't use -main-is. Can somebody confirm (or negate) this?
I believe this is partially true. However, it's a bit tricky; we don't actually generate a module. We merely emit a binding which claims to be from another module when typechecking what the user says should be the main module. Have a look at `Note [Root-main Id]` for details. On the whole this sounds fairly easy to do. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/13801#comment:2 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#13801: Make -main-is work with {thing} from arbitrary installed packages -------------------------------------+------------------------------------- Reporter: SimonHengel | Owner: (none) Type: feature request | Status: new Priority: normal | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by ezyang): So, are you aware that you can already refer to an identifier that is not defined in the Main module? The trick is that you have to export any of the identifiers you want to use. Here is a self-contained example: {{{ -- A.hs module A where f = print "This is A" -- Main.hs module Main(main, f) where import A main :: IO () main = return () }}} And then: `ghc --make Main.hs -main-is f`. Now, there are still things you might want to do... **You might want to not have to explicitly export f.** Then you would have to modify `checkMainExported`, and figure out if there was a technical reason why we required this (quite possibly because if it isn't exported, we will dead code eliminate it.) Maybe you should implicitly export any identifier mentioned with `-main-is`. That sounds helpful. **You might not want to have to import A.** In this case, you have to modify this code: {{{ getMainFun :: DynFlags -> RdrName getMainFun dflags = case mainFunIs dflags of Just fn -> mkRdrUnqual (mkVarOccFS (mkFastString fn)) Nothing -> main_RDR_Unqual }}} You'll have to take the module name, run it through the module lookup mechanism to make a Module, and then make an Orig RdrName. **You don't want to have to write main at all.** I think this is what you actually were asking for, based on the ticket text. In that case, GHC has to know that, if you pass `-main-is` and an empty list of modules, it should generate an empty source file (for Main), and then attempt to build that. You'd have to implement the previous step before that too. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/13801#comment:3 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler
participants (1)
-
GHC