Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can ghci reoder IO actions within unsafePerformIO IO blocks

Tags:

haskell

ghci

Can IO actions in IO blocks call within unsafePerformIO be reordered?

I have effectively the IO function.

assembleInsts :: ... -> IO S.ByteString
assembleInsts ... = do
    tmpInputFile <- generateUniqueTmpFile
    writeFile tmpInputFile str
    (ec,out,err) <- readProcessWithExitCode asm_exe [tmpInputFile] ""
    -- asm generates binary output in tmpOutputFile
    removeFile tmpInputFile
    let tmpOutputFile = replaceExtension tmpIsaFile "bits" -- assembler creates this
    bs <- S.readFile tmpOutputFile -- fails due to tmpOutputFile not existing
    removeFile tmpOutputFile
    return bs

where S.ByteString is a strict byte string.

Sadly, I need to call this in a tree of pure code far from the IO monad, but since I the assembler behaves as a referentially transparent (given unique files) tool, I figured for the time being I could make an unsafe interface for the time being.

{-# NOINLINE assembleInstsUnsafe #-}
assembleInstsUnsafe :: ... -> S.ByteString
assembleInstsUnsafe args = unsafePerformIO (assembleInsts args)

In addition I added to the top of the module the following annotation as per the documentation's (System.IO.Unsafe's) instructions.

{-# OPTIONS -fno-cse #-}
module Gen.IsaAsm where

(I tried to also add -fnofull-laziness as well, as per a reference that I consulted, but this was rejected by the compiler. I don't think that case applies here though.)

Running in ghci it reports the following error.

*** Exception: C:\Users\trbauer\AppData\Local\Temp\tempfile_13516_0.dat: openBinaryFile: does not exist (No such file or directory)

But if I remove removeFile tmpOutputFile, then it magically works. Hence, it seems like the removeFile is executing ahead of the process termination. Is this possible? The bytestring is strict, and I even tried to force the output at one point with a:

S.length bs `seq` return ()

before the removeFile.

Is there a way to dump intermediate code to find out what's going on? (Maybe I can trace this with Process Monitor or something to find out.) Unfortunately, I'd like to clean up within this operation (remove the file).

I think the exe version might work, but under ghci it fails (interpreted). I am using GHC 7.6.3 from the last Haskell Platform.

I know unsafePerformIO is a really big hammer and has other risks associated with it, but it would really limit the complexity of my software change.

like image 770
Tim Avatar asked Jul 30 '14 01:07

Tim


3 Answers

This may not be applicable, since it is based on assumptions unspecified in your question. In particular, this answer is based on the following two assumptions. S, which is unspecified, is Data.ByteString.Lazy and tmpDatFile, which is undefined, is tmpOutputFile.

import qualified Data.ByteString.Lazy as S
...
    let tmpDatFile = tmpOutputFile

Possible Cause

If these assumptions are true, removeFile will run too early, even without the use of unsafePerformIO. The following code

import System.Directory
import qualified Data.ByteString.Lazy as S

assembleInsts = do
    -- prepare a file, like asm might have generated
    let tmpOutputFile = "dataFile.txt"
    writeFile tmpOutputFile "a bit of text"
    -- read the prepared file 
    let tmpDatFile = tmpOutputFile
    bs <- S.readFile tmpOutputFile
    removeFile tmpDatFile
    return bs

main = do
    bs <- assembleInsts
    print bs

Results in the error

lazyIOfail.hs: DeleteFile "dataFile.txt": permission denied (The process cannot access the file because it is being used by another process.)

Removing the line removeFile tmpDatFile will make this code execute correctly, just like you describe, but leaving behind the temporary file isn't what is desired.

Possible Solution

Changing the import S to

import qualified Data.ByteString as S

instead results in the correct output,

"a bit of text".

Explanation

The documentation for Data.ByteSting.Lazy's readFile states that it will

Read an entire file lazily into a ByteString. The Handle will be held open until EOF is encountered.

Internally, readfile accomplishes this by calling unsafeInterleaveIO. unsafeInterleaveIO defers execution of the IO code until the term it returns is evaluated.

hGetContentsN :: Int -> Handle -> IO ByteString
hGetContentsN k h = lazyRead -- TODO close on exceptions
  where
    lazyRead = unsafeInterleaveIO loop

    loop = do
        c <- S.hGetSome h k -- only blocks if there is no data available
        if S.null c
          then do hClose h >> return Empty
          else do cs <- lazyRead
                  return (Chunk c cs)

Because nothing tries to look at the constructor of the bs defined in the example above until it is printed, which doesn't happen until after removeFile has been executed, no chunks are read from the file (and the file is not closed) before removeFile is executed. Therefore, when removeFile is executed, the Handle opened by readFile is still open, and the file can't be removed.

like image 149
Cirdec Avatar answered Oct 21 '22 13:10

Cirdec


Even if you are using unsafePerformIO, IO actions should not be reordered. If you want to be sure of that, you can use the -ddump-simpl flag to see the intermediate Core language which GHC produces, or even one of the other -dump-* flags showing all the compilation intermediate steps up to assembly.

I am aware that this answers what you asked, and not what you actually need, but you can rule out GHC bugs at least. It seems unlikely there's a bug affecting this in GHC, though.

like image 26
chi Avatar answered Oct 21 '22 13:10

chi


Totally my fault.... sorry everyone. GHC does not reorder IO actions in an IO block under the above stated conditions as mentioned by those above. The assembler was just failing to assemble the output and create the assumed file. I simply forgot to check the exit code or the output stream of the assembler. I assumed the input to be syntactically correct since it is generated, the assembler rejected it and simply failed to create the file. It gave a valid error code and error diagnostic too, so that was really bad on my part. I may have been using readProcess the first time around, which raises an exception on a non-zero exit, but must have eventually changed this. I think the assembler had a bug where it didn't correctly indicate a failing exit code for some cases, and I had to change from readProcessWithExitCode.

I am still not sure why the error went away when I elided the removeFile.

I thought about deleting the question, but I a hoping the suggestions above help others debug similar (more valid) problems as well. I've been burned by the lazy IO thing Cirdec mentioned, and the -ddump-simpl flag mentioned by chi is good to know as well.

like image 43
Tim Avatar answered Oct 21 '22 14:10

Tim