Subject: Separate Compilation

From: [email-masked]
Date: Mon, 11 Jun 90 16:47:26 EDT

----------------------------Original message----------------------------
Here's some documentation for the "import" command of SML of NJ.
     
        Separate Compilation in SML of NJ Version 0.56
     
                 Tim Freeman
     
           Last Modified Mon Jun 11 16:35:11 1990.
     
When people load source files with the "use" command, SML reads the
source file, then it compiles the source file only to memory.
Separate compilation in SML instead saves the results of compilation
in a file.  This saves time if the same file is reloaded later.
     
SML of NJ supports separate compilation with the import command.  This
command has the syntax
     
   import "filename";
     
If you give this command to SML, it will first look for a file
"filename.bin" which, if it exists, contains the binary results of
compilation.  If "filename.bin" exists and is not out of date, SML
will load it.  If "filename.bin" does not exist or is out of date, SML
will compile source code in "filename.sml", creating "filename.bin",
then it will load "filename.bin".
     
Only one file may be imported per import command.  If you have several
files, you must use several import commands.
     
Notice that this is the first time a naming convention has been
enforced for SML source files.  If you want to use separate
compilation, your source file name must end in ".sml", and if you are
perverse enough to give a source file a name ending in ".bin", it may
get clobbered.
     
SML does the right thing when deciding whether a file is out of date,
even if that file imports other files (which in turn may import other
files).
     
Files to be separately compiled should contain only functors,
structures, and signatures.  To minimize the amount of recompilation,
it is usually best to put exactly one functor, structure, or signature
in each file.  Customarily, a functor named "Foo" is placed in a file
"foo.sml", and if it has a signature associated with it, that
signature is placed in "foo.sig.sml".
     
Each separate compilation begins in the default environment, which is
the same as SML's environment when SML first starts.  If your code
needs to have certain signatures or functors defined before it can
compile, these signature or functor definitions must be imported at
the top of the source file.
     
Importing a functor ("foo.sml") doesn't automatically import the
corresponding signature ("foo.sig.sml").  In your source code, it's
best to only import signatures.  At top level, you only need to import
functors, since the top-level interpreter is willing to compose
imported functors without ever importing the signatures at top level.
     
There is an example at the end of this document.
     
        How to Convert to Separate Compilation
     
First, organize your source files into functors and signatures.  It
will minimize future recompilation if each functor only uses the
structures it takes as arguments.
     
Second, split your program up so that one signature or one functor
definition is in each source file.  Source files containing signatures
should have names ending in ".sig.sml"; other source files should have
names ending in ".sml".
     
Third, insert import statements so that each file imports the
signature definitions it needs.  It is also possible to import functor
definitions, but you will recompile things less often if you organize
things so this is unnecessary.  This is illustrated in the example
below.  If you are missing a necessary import statement, the compiler
will complain that the signature that would have been loaded by that
import statement is undefined.
     
Fourth, import the file containing the top-level functor from SML's
top level.  The compiler will try to compile everything, and it will
probably find places where you made mistakes in following steps one
through three above.  Fix those mistakes, and you'll be done.
     
                   Caveats
     
Don't do separate compilation in a directory where it is possible to
get write errors.  If SML gets an error while writing a .bin file, it
will ignore the error, thus leaving a corrupted .bin file.  What
usually happens next is that SML will crash with a segmentation fault
when it tries to load the corrupted .bin file.  Future invocations of
SML will then crash while trying to load the same .bin file, until you
delete the .bin file by hand and recompile.  SML may create several
corrupted .bin files at once, so I usually delete all .bin files when
I discover that any one of them causes SML to segmentation fault.
     
When you modify a signature that everything depends on, the dependency
analysis will correctly infer that everything needs to be recompiled,
and will go do it.  This can involve a lot of recompilation.
     
                   Example
     
We have these files:
     
disease.sml:
   import "body.sig";
   import "disease.sig";
   functor Disease (structure Body : BODY) : DISEASE =
       struct
       structure Body = Body
       open Body
       fun mononucleosis (Body b) =
           (#spleen b := Inflamed;
        #throat b := Sore)
       end
disease.sig.sml:
   import "body.sig";
   signature DISEASE =
       sig
       structure Body : BODY
       open Body
       val mononucleosis: body -> unit
       end
body.sml:
   import "body.sig";
   functor Body () =
       struct
       datatype organstate = Inflamed | Sore | Healthy
       datatype body =
              Body of {spleen: organstate ref, throat: organstate ref}
       fun newbody () = Body {spleen = ref Healthy, throat = ref Healthy}
       end
body.sig.sml:
   signature BODY =
       sig
       datatype organstate = Inflamed | Sore | Healthy
       datatype body =
           Body of {spleen: organstate ref, throat: organstate ref}
       val newbody: unit -> body
       end
link.sml:
   import "body";
   import "disease";
   structure Body = Body ()
   open Body
   structure Disease = Disease (structure Body = Body)
   open Disease
     
The following slightly edited terminal dialogue happened:
% sml
- use "link.sml";
    (* Compiled all of the .sml files except link.sml. *)
val it = () : unit
- val foo = newbody();
val foo = Body {spleen=ref Healthy,throat=ref Healthy} : body
- mononucleosis foo;
val it = () : unit
- foo;
val it = Body {spleen=ref Inflamed,throat=ref Sore} : body
- ^D
% touch body.sml
% sml
- use "link.sml";
    (* Recompiled body.sml into body.bin, loaded all other binary
    files.  This illustrates the advantage of having files depend on
    body.sig.sml instead of body.sml.  More recompilation would have
    happened if source files depended on body.sml. *)
val it = () : unit
- ^D
% touch body.sig.sml
% sml
- use "link.sml";
    (* Recompiled all of the .sml files except link.sml.  This
    illustrates the last caveat above, where a small modification can
    result in large amounts of recompilation. *)
val it = () : unit
- ^D
     
               Acknowledgements
     
Thanks to Scott Dietzen and Benjamin Pierce for reading this for me
and providing good feedback, and to Nevin Heintze for suggesting I
write it.