FlymakeHaskell

2007-11-11 revised the instruction : Daisuke IKEGAMI a.k.a ike

2007-11-26 fixed a bug of the perl script about printing warning messages and add thanks : ike

2008-05-08 change to use ‘-fbyte-code’ instead ‘-fno-code’ for supporting hierarchical modules : ike

2008-08-24 fixed to support spaces in source and base_dir paths : Chry Cheng

2009-01-27 pure elisp implementation using the builtin AOP to tweak flymake : Rohan Hart

2009-03-16 nothing worked for me, so I tried a simpler solution : John Wiegley

2010-01-26 reworked the elisp version as haskell-mode now provides some flymake integration (which I disable): Rohan Hart

2010-05-11 Perl script that builds using Cabal, if possible: Florian Lorenzen

I’ve tested the following procedure on both GNU Emacs 21.2.1 and 22.1.50.1. I’m afraid that it may not work on another version. Please revise this document if you find a problem or its solution.

Introduction

Quoting what is flymake-mode from the page FlyMake:

Flymake is implemented as an Emacs minor mode. It runs the syntax check tool in the background, passing it a temporary copy of the current buffer, and parses the output for known error/warning message patterns. Flymake then highlights erroneous lines (that is, lines for which at least one error or warning has been reported), and displays an overall buffer status in the mode line.

See also FlyMake for details.

There are some videos for demonstrations.

These are too slow to see because my laptop is old and slow. Flymake must work more faster on the latest computer.

This page describes how to use Flymake for Haskell to check the syntax and type by invoking ghc.

How to install

Preliminaries

Before changing your .emacs, put the following perl script in ‘exec-path’. If you don’t know about ‘exec-path’, then try to type ‘M-x describe-variable’ and then type again ‘exec-path’ on your emacs.

Unfortunately, since MS-Windows does not have perl, MS-Windows users have to install perl itself, too. See the following page for more information: http://www.perl.com/download.csp#win32

The following checker passes ‘-Wall’ to ghc to display every warnings. If you don’t want to notice some warning, then reedit the script.

Don’t forget the perl script to set executable itself as a command. (This can be done in Linux e.g. with “chmod a+x flycheck_haskell.pl”)

#!/usr/bin/env perl
# flycheck_haskell.pl

### Please rewrite the following 3 variables 
### ($ghc, @ghc_options and @ghc_packages)

$ghc = '/usr/local/bin/ghc'; # where is ghc
@ghc_options  = ('-Wall');   # e.g. ('-fglasgow-exts')
@ghc_packages = ();          # e.g. ('QuickCheck')

### the following should not been edited ###

use File::Temp qw /tempfile tempdir/;
File::Temp->safe_level( File::Temp::HIGH );

($source, $base_dir) = @ARGV;

$source =~ s/ /\\ /g;
$base_dir =~ s/ /\\ /g;

@command = ($ghc,
            '--make',
            '-fbyte-code',
            "-i$base_dir",
            "-i$base_dir/..",
            "-i$base_dir/../..",
            "-i$base_dir/../../..",
            "-i$base_dir/../../../..",
            "-i$base_dir/../../../../..",
            $source);

while(@ghc_options) {
    push(@command, shift @ghc_options);
}

while(@ghc_packages) {
    push(@command, '-package');
    push(@command, shift @ghc_packages);
}

$dir = tempdir( CLEANUP => 1 );
($fh, $filename) = tempfile( DIR => $dir );

system("@command >$filename 2>&1");
open(MESSAGE, $filename); 
while(<MESSAGE>) {
    if(/(^\S+\.hs|\.lhs)(:\d*:\d*:)\s?(.*)/) {
        print "\n";
        print $1;
        print $2;
        $rest = $3;
        chomp $rest;
        print $rest;
        next;
    }
    if(/\s+(.*)/) {
        $rest = $1;
        chomp $rest;
        print $rest;
        print " ";
        next;
    }
}
close($fh);
print "\n";

Version of your emacs

To determine the version, you can type ‘M-x version RET’ in emacs. Note that the configuration differs for Emacs21 and Emacs22.

Instructions for Emacs21

Since Emacs21 does not have ‘flymake-mode’, you have to install it at first. Because of the several reasons, I don’t recommend you the latest version of flymake-mode, but to use the revision 1.46 of the Main branch. You can get the library from the repository of ‘flymake’;

http://cvs.savannah.gnu.org/viewvc/*checkout*/emacs/emacs/lisp/progmodes/flymake.el?revision=1.46

Because Emacs21 does not have the variable ‘compilation-error-regexp-alist-alist’, we have to edit the flymake.el as follows:

--- flymake.el  2007-11-11 05:48:41.000000000 +0900
+++ /usr/local/share/emacs/site-lisp/flymake.el 2007-11-09 10:29:31.000000000 +0900
@@ -929,8 +929,8 @@
      ;; ant/javac
      (" *\\(\\[javac\\]\\)? *\\(\\([a-zA-Z]:\\)?[^:(\t\n]+\\)\:\\([0-9]+\\)\:[ \t\n]*\\(.+\\)"
       2 4 nil 5))
-   ;; compilation-error-regexp-alist)
-   (flymake-reformat-err-line-patterns-from-compile-el compilation-error-regexp-alist-alist))
+   compilation-error-regexp-alist)
+   ;; (flymake-reformat-err-line-patterns-from-compile-el compilation-error-regexp-alist-alist))
   "Patterns for matching error/warning lines.  Each pattern has the form
 \(REGEXP FILE-IDX LINE-IDX COL-IDX ERR-TEXT-IDX).
 Use `flymake-reformat-err-line-patterns-from-compile-el' to add patterns

Finally, put the edited flymake.el into your ‘load-path’. If you don’t know about ‘load-path’, then try to type ‘M-x describe-variable’ and then type again ‘load-path’ on your emacs.

About Emacs22

Because Emacs22 has flymake-mode, you don’t need to install ‘flymake.el’ by yourself.

Setting flymake-mode for Haskell

Put the followings into your .emacs.

    (require 'flymake)
    (defun flymake-Haskell-init ()
          (flymake-simple-make-init-impl
            'flymake-create-temp-with-folder-structure nil nil
            (file-name-nondirectory buffer-file-name)
            'flymake-get-Haskell-cmdline))
    (defun flymake-get-Haskell-cmdline (source base-dir)
       (list "flycheck_haskell.pl"
            (list source base-dir)))
    
    (push '(".+\\.hs$" flymake-Haskell-init flymake-simple-java-cleanup)
          flymake-allowed-file-name-masks)
    (push '(".+\\.lhs$" flymake-Haskell-init flymake-simple-java-cleanup)
          flymake-allowed-file-name-masks)
    (push
      '("^\\(\.+\.hs\\|\.lhs\\):\\([0-9]+\\):\\([0-9]+\\):\\(.+\\)"
       1 2 3 4) flymake-err-line-patterns)
    ;; optional setting
    ;; if you want to use flymake always, then add the following hook.
    ;; (add-hook
    ;;  'haskell-mode-hook
    ;;  '(lambda ()
    ;;     (if (not (null buffer-file-name)) (flymake-mode))))

Tips

Display messages at the minibuffer

Add the following setting to see the error/warning message at the minibuffer.

    (when (fboundp 'resize-minibuffer-mode) ; for old emacs
      (resize-minibuffer-mode)
      (setq resize-minibuffer-window-exactly nil))

The following code is introduced at

http://www.credmp.org/index.php/2007/07/20/on-the-fly-syntax-checking-java-in-emacs/

    (defun credmp/flymake-display-err-minibuf () 
      "Displays the error/warning for the current line in the minibuffer"
      (interactive)
      (let* ((line-no             (flymake-current-line-no))
             (line-err-info-list  (nth 0 (flymake-find-err-info flymake-err-info line-no)))
             (count               (length line-err-info-list))
             )
        (while (> count 0)
           (when line-err-info-list
           (let* ((file       (flymake-ler-file (nth (1- count) line-err-info-list)))
                   (full-file  (flymake-ler-full-file (nth (1- count) line-err-info-list)))
                   (text (flymake-ler-text (nth (1- count) line-err-info-list)))
                   (line       (flymake-ler-line (nth (1- count) line-err-info-list))))
              (message "[%s] %s" line text)
              )
            )
          (setq count (1- count)))))

Bind the above function to the key ‘\C-c d’.

    (add-hook
     'haskell-mode-hook
     '(lambda ()
        (define-key haskell-mode-map "\C-cd"
          'credmp/flymake-display-err-minibuf)))

FAQ : Why perl and the perl script are needed?

The flymake-mode assumes that the checker displays error massages as one line at the version 0.3 when I write this sentence at 2007-11-11.

Alternative version using pure elisp AOP

This works for flymake 0.3. One limitation compared to using Perl is that if the split string parameter to flymake-split-string should change position in some future version then the advice will need to be changed accordingly. Another is that, since flymake uses a process filter, this relies on ghc outputting entire errors and the text not being chunked by the emacs runtime in same way - I’ve only tested this using ghc 6.10.1 on Windows and ghc 6.10.4 on Linux.

Comment out these lines in haskell-mode.el

    (eval-after-load "flymake"
      '(add-to-list 'flymake-allowed-file-name-masks
                '("\\.l?hs\\'" haskell-flymake-init)))

Add the following to your .emacs

    (defun flymake-Haskell-init ()
          (flymake-simple-make-init-impl
            'flymake-create-temp-with-folder-structure nil nil
            (file-name-nondirectory buffer-file-name)
            'flymake-get-Haskell-cmdline))
    
    (defun flymake-get-Haskell-cmdline (source base-dir)
      (list "ghc"
        (list "--make" "-fbyte-code"
              (concat "-i"base-dir)  ;;; can be expanded for additional -i options as in the Perl script
              source)))
    
    (defvar multiline-flymake-mode nil)
    (defvar flymake-split-output-multiline nil)
    
    ;; this needs to be advised as flymake-split-string is used in other places and I don't know of a better way to get at the caller's details
    (defadvice flymake-split-output
      (around flymake-split-output-multiline activate protect)
      (if multiline-flymake-mode
          (let ((flymake-split-output-multiline t))
            ad-do-it)
        ad-do-it))
    
    (defadvice flymake-split-string
      (before flymake-split-string-multiline activate)
      (when flymake-split-output-multiline
        (ad-set-arg 1 "^\\s *$")))
    
    ;; Why did nobody tell me about eval-after-load - very useful
    (eval-after-load "flymake"
      '(progn
         (add-to-list 'flymake-allowed-file-name-masks
                  '("\\.l?hs$" flymake-Haskell-init flymake-simple-java-cleanup))
         (add-to-list 'flymake-err-line-patterns
                  '("^\\(.+\\.l?hs\\):\\([0-9]+\\):\\([0-9]+\\):\\(\\(?:.\\|\\W\\)+\\)"
                    1 2 3 4))))
    
    (add-hook
     'haskell-mode-hook
     '(lambda ()
        (set (make-local-variable 'multiline-flymake-mode) t)))

Acknowledgments

The authors would like to thank Thomas Schilling, Gwern Branwen and Denis Bueno for useful suggestions. Credmp’s hack for the mini-buffer is cool and very useful. We would also like to thank the developer team of flymake-mode and the anonymous reviewers.

Alternative Perl script to use Cabal

The following version of the flycheck_haskell.pl script first looks for a Cabal and Setup.[l]hs file. If successful, it uses runhaskell Setup.[l]hs build to run the compiler; otherwise, ghc --make is used as fallback.

    #!/usr/bin/env perl
    # flycheck_haskell.pl
    ### Please rewrite the following 3 variables 
    ### ($ghc, @ghc_options and @ghc_packages)
    $runhaskell = 'runhaskell';  # where is runhaskell (for Cabal)
    $ghc = 'ghc';                # where is ghc
    @ghc_options  = ();          # e.g. ('-fglasgow-exts')
    @ghc_packages = ();          # e.g. ('QuickCheck')
    ### the following should not been edited ###
    use File::Temp qw /tempfile tempdir/;
    File::Temp->safe_level( File::Temp::HIGH );
    ($source, $base_dir) = @ARGV;
    $source =~ s/ /\\ /g;
    $base_dir =~ s/ /\\ /g;
    $source = glob $source;
    $base_dir = glob $base_dir;
    ### Check if the project is Cabalized. We simply look for a
    ### Setup.[l]lhs and a *.cabal file in the current directory and three
    ### levels up the filesystem.
    @levels = qw|. .. ../.. ../../..|;
    undef $cabaldir;
    foreach $up (@levels) {
        undef @setup;
        undef @cabal;
        opendir DIR, "$base_dir/$up";
        @files = readdir DIR;
        closedir DIR;
        @setup = grep /^Setup\.(lhs|hs)$/, @files;
        @cabal = grep /^.+\.cabal$/, @files;
        if($#setup == 0 && $#cabal == 0) {
            $cabaldir = "$base_dir/$up";
            last;
        }
    }
    if($cabaldir) {
        # Cabal file found. Use Setup.[l]hs to build.
        @command = ('cd',
                    $cabaldir,
                    '&&',
                    $runhaskell,
                    $setup[0],
                    'build');
        
    }
    else {
        # No cabal file found. Use ghc --make.
        @command = ($ghc,
                    '--make',
                    '-fbyte-code',
                    "-i$base_dir",
                    "-i$base_dir/..",
                    "-i$base_dir/../..",
                    "-i$base_dir/../../..",
                    "-i$base_dir/../../../..",
                    "-i$base_dir/../../../../..",
                    $source);
        while(@ghc_options) {
            push(@command, shift @ghc_options);
        }
        while(@ghc_packages) {
            push(@command, '-package');
            push(@command, shift @ghc_packages);
        }
    }
    $dir = tempdir( CLEANUP => 1 );
    ($fh, $filename) = tempfile( DIR => $dir );
    system("@command >$filename 2>&1");
    open(MESSAGE, $filename);
    undef $processing;
    while(<MESSAGE>) {
        if(/(^\S+)(\.hs|\.lhs)(:\d*:\d*:)\s?(.*)/) {
            $file = "$1$2";
            # We print only those error messages that are in the source
            # file passed on the command line.
            if(substr($source, -length($file)) eq $file) {
                $processing = 1;
                print "\n";
                print $source;
                print $3;
                $rest = $4;
                chomp $rest;
                print $rest;
                next;
            }
            else {
                undef $processing;
            }
        }
        if($processing) {
            if(/\s+(.*)/) {
                $rest = $1;
                chomp $rest;
                print $rest;
                print " ";
                next;
            }
        }
    }
    close($fh);
    print "\n";

Note

Please write comments on the above instruction here.


Historical note before the method was found

Second implementation: Gwern Branwen found troubles at the first implementation and developed another better one.

    (defun flymake-Haskell-init () 
      (let* ((temp-file       (flymake-init-create-temp-buffer-copy 
                               'flymake-create-temp-inplace)) 
             (local-file  (file-relative-name 
                           temp-file 
                           (file-name-directory buffer-file-name)))) 
        (list "ghc" (list "--make" "-fno-code" local-file "-i")))) 

What were the troubles?DenisBueno


First implementation: http://madscientist.jp/~ikegami/diary/20071108.html#p01

Thomas Schilling suggests the '-fno-code' flag for speed-up. Thanks Thomas! --ike

I noticed that this does not work with Emacs 22.1.50.1. Anyone helps? However, with GNU Emacs 21.2.1, it works. I don't know the difference. – ike

    (require 'flymake)
    
    ;; flymake for Haskell
    (defun flymake-Haskell-init ()
      (flymake-simple-make-init-impl
        'flymake-create-temp-with-folder-structure nil nil
        buffer-file-name
        'flymake-get-Haskell-cmdline))
    (defun flymake-get-Haskell-cmdline (source base-dir)
      (list "ghc" (list "--make" "-fno-code" (concat "-i" base-dir) source)))          
    (push '(".+\\.[hg]s$" flymake-Haskell-init)
      flymake-allowed-file-name-masks)
    (push '(".+\\.l[hg]$" flymake-Haskell-init)
      flymake-allowed-file-name-masks)
    (push '("^\\(\.+\.hs\\|\.lhs\\):\\([0-9]+\\):\\([0-9]+\\):\\(.+\\)"
       1 2 3 4) flymake-err-line-patterns)

With the regexp above, my reported warnings look like "15:" -- without actual warning text. Emacs 22.1.2. Thoughts?DenisBueno

To DenisBueno: Yes, the warning text is not displayed at now 2007-11-08 because of the regexp seems wrong. The problem also seems to depend the version and branch of the repository of flymake.el – ike

I may grasp the problem while reading the function 'flymake-parse-err-lines' in flymake.el of the HEAD branch at 2007-11-09. Because 'flymake-parse-err-lines' assumes that the error or warning message is one-line, I think, the rest of messages are ignored. If I've misunderstood, please write comments. To solve the problem, we need a tool to reedit the output messages of ghc. – ike

That's probably the path of least resistance. I'm not sure why flymake matches each line individually; it could just match on the input until there are no more matches. Then the regexps themselves could control whether or not they match multiple lines. But this would involve changes in flymake.DenisBueno

If you want to add some options or packages into ghc flags, then try to replace ‘flymake-get-Haskell-cmdline’ with the following;

    (defvar flymake-ghc-options (list "-Wall"))
    (defvar flymake-ghc-packages
       (mapcar (lambda (p) (concat "-package " p))
         '("QuickCheck")))
    (defun flymake-get-Haskell-cmdline (source base-dir)
       (list "ghc"
          (append
             (list "--make" "-fno-code" (concat "-i" base-dir) source)
             flymake-ghc-options
             flymake-ghc-packages)))

Here is a perl program which prints error messages as a line:

    #!/usr/bin/env perl
    #
    
    $ghc = '/usr/local/bin/ghc'; # where is ghc
    @ghc_options  = ();          # e.g. ('-fglasgow-exts')
    @ghc_packages = ();          # e.g. ('QuickCheck')
    
    ### the following should not been edited ###
    
    use File::Temp qw /tempfile tempdir/;
    File::Temp->safe_level( File::Temp::HIGH );
    
    ($source, $base_dir) = @ARGV;
    
    @command = ($ghc,
                '--make',
                '-fno-code',
                "-i$base_dir",
                $source);
    
    while(@ghc_options) {
        push(@command, shift @ghc_options);
    }
    
    while(@ghc_packages) {
        push(@command, '-package');
        push(@command, shift @ghc_packages);
    }
    
    $dir = tempdir( CLEANUP => 1 );
    ($fh, $filename) = tempfile( DIR => $dir );
    
    system("@command >$filename 2>&1");
    open(MESSAGE, $filename); 
    while(<MESSAGE>) {
        # this code is wrong - you need a grouping around the .hs|.lhs bit if you
        # want it to properly notice lhs files and point to the right place
        # if(/^\S+\.hs|\.lhs:\d*:\d*:.*/) {
        if(/^\S+(?:\.hs|\.lhs):\d*:\d*:.*/) {
        print "\n";
        chomp $_;
        print $_;
        next;
        }
        if(/\s+(.*)/) {
        $rest = $1;
        chomp $rest;
        print $rest;
        print " ";
        next;
        }
    }
    close($fh);
    print "\n";

Then, we can use this checker;

    (defun flymake-get-Haskell-cmdline (source base-dir)
      (list "flycheck_haskell.pl"
        (list source base-dir)))
    (push
      '("^\\(\.+\.hs\\|\.lhs\\):\\([0-9]+\\):\\([0-9]+\\):\\(.+\\)"                                                                                
      1 2 3 4) flymake-err-line-patterns)

Here’s what it took for me to get things working. All of the solutions above were permanently hanging my Emacs on OS X, so here’s what I ended up having to use:

Perl script, name it “hslint”:

    #!/usr/bin/env perl
    
    $ghc = '/opt/local/bin/ghc'; # where is ghc
    
    @ghc_options  = ('-Wall');   # e.g. ('-fglasgow-exts')
    @ghc_packages = ();          # e.g. ('QuickCheck')
    
    ### the following should not been edited ###
    
    @command = ($ghc, '--make', '-fbyte-code', $ARGV[0]);
    
    while (@ghc_options) {
        push(@command, shift @ghc_options);
    }
    
    while (@ghc_packages) {
        push(@command, '-package');
        push(@command, shift @ghc_packages);
    }
    
    open(MESSAGE, "@command 2>&1 |");
    
    while (<MESSAGE>) {
        if(/(^\S+\.l?hs)(:\d*:\d*:)\s?(.*)/) {
            print "\n";
            print $1;
            print $2;
            $rest = $3;
            chomp $rest;
            print $rest;
        }
        if(/^\s+(.+)/) {
            $rest = $1;
            chomp $rest;
            print $rest;
            print " ";
        }
    }
    close MESSAGE;
    
    print "\n";

Then add this to your .emacs:

    (autoload 'flymake-mode "flymake" "" t)
    
    (eval-after-load "flymake"
      '(progn
         (defun flymake-hslint-init ()
           (let* ((temp-file   (flymake-init-create-temp-buffer-copy
                                'flymake-create-temp-inplace))
                  (local-file  (file-relative-name
                                temp-file
                                (file-name-directory buffer-file-name))))
             (list "hslint" (list local-file))))
    
         (add-to-list 'flymake-allowed-file-name-masks
                      '("\\.l?hs\\'" flymake-hslint-init))))

Now just run “M-x flymake-mode” in any Haskell buffer to turn on Flymake.


Just because the awfulness of that perl script has been bugging me for ages:

   #!/usr/bin/env perl
   
    $ghc = '/usr/local/bin/ghc'; # where is ghc
   
    @ghc_options  = qw(-Wall);   # e.g. ('-fglasgow-exts')
    @ghc_packages = qw(ghc);          # e.g. ('QuickCheck')    
    ### the following should not been edited ###
    my $src = $ARGV[0];
    my @command = ($ghc, qw{--make -fbyte-code}, $src, @ghc_options, join(' -package ','', @ghc_packages));
    open(MESSAGE, "@command 2>&1 |");
    while (<MESSAGE>) {
        chomp;
        if (/(^\S+\.l?hs)(:\d*:\d*:)\s?(.*)$/) {
            print "\n$1$2$3";
        } elsif (/^\s+(.+)$/) {
            print "$1 ";
        }
    }
    close MESSAGE;

FlyMake


Troubleshooting

Flymake don't show up errors on single Haskell files

(Hartmut 2011-07-11)

Set flymake log-level to 3: In .emacs configuration enter after the “(custom-set-variables” statement: ‘(flymake-log-level 3) or do a M-x customize-group flymake and set the log level to 3. After doing a erroneous change to a Haskell source file, do have a look at emacs’ Messages buffer - if there appears “received 1 byte(s) of output from process …” (only 1 Byte! that is the point, receiving 1 byte is only ok if there are no errors) then I guess you have space characters in your Haskell source file. After renaming your source file, the flymake runs as a charm. (I am using the “Alternative Perl script to use Cabal”, see above).