#line 79 "/home/travis/build/felix-lang/felix/src/packages/flx.fdoc"
  class FlxCache
  {
    fun gramtime(debugln: string -> 0) (path:string, s:string) : double = {
      //println$ "Path=" + path + " file = " + s;
      fun maxtime (x:double) (s:string) => max (x, gramtime debugln (path, s));
      if s.[0]=="@".char do
        var file =
          let f = s.[1 to].strip in
          if Filename::is_absolute_filename f then f
          else Directory::mk_absolute_filename (Filename::join$ path, f)
        ;
        var filetime = FileStat::dfiletime(file,0.0);
        if filetime == 0.0 do
          println$ "Grammar include file '" + file "' doesn't exist, exiting";
          System::exit 1;
        done
        debugln$ "Grammar include file '" + file + "' time=" + FileStat::strfiletime(filetime);
        var filetext = load file;
        var files = split (filetext, "\n");
        files = map strip of (string) files;
        files = filter (fun (s:string) => s != "") files;
        files = map (fun (s:string) => Filename::join (split(s,"/"))) files;
        //println$ "Files=" + files;
        return fold_left maxtime filetime files;
      else
        file = Filename::join$ path, s;
        filetime = FileStat::dfiletime(file,0.0);
        if filetime == 0.0 do
          println$ "Grammar file " + file " doesn't exist, exiting";
          System::exit 1;
        done
        debugln$ "Grammar file " + file + " time=" + FileStat::strfiletime(filetime);
        return filetime;
      done
    }
  
    // FLX_INSTALL_DIR: root for finding standard grammar
    // STDGRAMMAR: root standard grammar key, within FLX_INSTALL_DIR
    //      usually "grammar/grammar.files"
    // FLXG: absolute filename of felix compiler executable
  
    // CACHE_DIR: absolute filename of binary cache
    // OUTPUT_DIR: absolute filename of text cache
  
    // DEFAULT_CACHE_DIR: default location of CACHE_DIR
    // DEFAULT_OUTPUT_DIR: default location of OUTPUT_DIR
    //    These defaults are used to determine if the
    //    the cache should be deleted automatically
    //    or a an interactive query used to verify.
    //    Automatic deletion requies the caches to be the default.
    // CLEAR_CACHE: switch to force clearing the cache
  
    typedef cache_validation_spec_t =
    (
       FLX_SHARE_DIR:string,
       GRAMMAR_DIR:string,
       STDGRAMMAR:string,
       FLXG:string,
       CACHE_DIR:string,
       OUTPUT_DIR:string,
       CLEAR_CACHE: int,
       AUTOMATON: string,
       debugln : string -> 0,
       xqt: string -> string,
       quote: string -> string
    );
  
  
    // CACHE VALIDATION
    //
    // This function validates the current cache, and if it is considered
    // stale may flush it. If the cache is the default one in the users
    // home directory the flush is done noisily but unconditionally.
    // Otherwise the user is prompted for permission.
    // The special cache locations / and . or "" are never deleted
    // in case it wipes out parts of the root, home, or current directory.
  
    // The validation checks the time of the flxg compiler used to build
    // it against the current flxg compiler, these must be exactly equal.
    //
    // It also checks that all the files defining the grammar are older
    // than the generated automaton.
    //
    // It does NOT check any RTL C++ libraries are up to date.
    // It does NOT check any Felix program files are up to date.
    // Therefore it does NOT guarrantee the contents of the cache are valid.
    // Rather it ensures only that the compiler and cached automaton are not stale.
    // However if they are stale the whole cache is invalidated.
    //
    // In effect this means this function ensures the parser is ready and valid
    // or non-existant. The compiler and automaton are locked together. If the compiler
    // changes the automaton must be rebuilt.
  
    // returns cache time
    gen validate_cache  (spec: cache_validation_spec_t) : int * double =
    {
  
      // ensure the cache directory exists
      Directory::mkdirs(spec.CACHE_DIR);
  
      // get the OS timestamp of the flxg compiler, +inf if not found
      var flxg_time = FileStat::dfiletime(spec.FLXG, #FileStat::future_time);
      spec.debugln$ "Flxg=" + spec.FLXG;
      spec.debugln$ "Flxg_time=" + FileStat::strfiletime(flxg_time);
  
      // get the OS timestamp of the file flxg_time.stamp
      // this file is created with the cache
      var flxg_stamp = Filename::join spec.CACHE_DIR "flxg_time.stamp";
      var cache_time = FileStat::dfiletime(flxg_stamp,#FileStat::future_time);
      spec.debugln$ "cache_time=" + FileStat::strfiletime(cache_time);
  
      // get the timestamp string recorded in flxg_time.stamp
      var flxg_stamp_data = load flxg_stamp;
      //println$ "Flxg_stamp_data=" + flxg_stamp_data;
  
      // convert the timestamp string to a double, if there is junk
      // there or the string is empty, 0.0 is returned by atof,
      // adjust that to -inf
      var flxg_stamp_time = match flxg_stamp_data.atof with | 0.0 => #FileStat::past_time | x => x;
  
      spec.debugln$ "Flxg_stamp_data : " + FileStat::strfiletime(flxg_stamp_time);
  
      // Calculate the time of the newest text file defining the grammar
      // these are files in directory share/lib/grammar.
      var grammar_time = gramtime spec.debugln (spec.GRAMMAR_DIR, "@"+spec.STDGRAMMAR);
      spec.debugln$ "Grammar text time=" + FileStat::strfiletime (grammar_time);
  
      // calculate the name of the compiled grammar automaton in the cache
      var automaton_name = spec.AUTOMATON;
  
      // Get the timestamp of the grammar automaton or -inf if it doesn't exist.
      var automaton_time = FileStat::dfiletime(automaton_name,#FileStat::past_time);
      spec.debugln$ "Automaton " + automaton_name + " time=" + FileStat::strfiletime(automaton_time);
  
      // If the cache exists and the recorded compiler time stamp is not equal
      // to the current compiler time stamp, then the cache is stale
      // and should be deleted.
      if cache_time != #FileStat::future_time and flxg_stamp_time != flxg_time do
        println$ "Cache may be out of date due to compiler change!";
        println$ "Flxg compiler time stamp=" + FileStat::strfiletime(flxg_time);
        println$ "Cache time stamp        =" + FileStat::strfiletime(cache_time);
  
        // special safety check if the output dirs are root or current directory
        if not (
          (spec.OUTPUT_DIR == "/" or spec.OUTPUT_DIR == "" or spec.OUTPUT_DIR == ".") or
          (spec.CACHE_DIR == "/" or spec.CACHE_DIR == "" or spec.CACHE_DIR == ".")
        )
        do
          spec.CLEAR_CACHE = 1;
        done
  
      // If the automaton exists and the grammar is newer than the automaton
      // then the cache is stale and should be deleted.
      elif grammar_time > automaton_time do
        println$ "Cache may be out of date due to grammar upgrade!";
        println$ "Grammar time stamp          =" + FileStat::strfiletime(grammar_time);
        println$ "Automaton.syntax time stamp =" + FileStat::strfiletime(automaton_time);
        spec.CLEAR_CACHE = 1;
      done
  
      // FFF BE CAREFUL! The value "/" for these caches is perfectly good
      if spec.CLEAR_CACHE != 0 do
        // refuse to delete "" or "/" or ".", basic safety check
        if
          (spec.OUTPUT_DIR == "/" or spec.OUTPUT_DIR == "" or spec.OUTPUT_DIR == ".") or
          (spec.CACHE_DIR == "/" or spec.CACHE_DIR == "" or spec.CACHE_DIR == ".")
        do
          println "WILL NOT DELETE CACHES";
          println$ "output cache " + spec.OUTPUT_DIR;
          println$ "binary cache " + spec.CACHE_DIR;
          System::exit(1);
        done
  
        println$ "Delete cache " + spec.OUTPUT_DIR;
        if PLAT_WIN32 do
            C_hack::ignore$ spec.xqt("mkdir "+spec.quote(spec.OUTPUT_DIR+"\\rubbish") +"& rmdir /Q /S " + spec.quote(spec.OUTPUT_DIR));
        else
            C_hack::ignore$ spec.xqt("rm -rf " + spec.quote(spec.OUTPUT_DIR));
        done
        println$ "Delete cache " + spec.CACHE_DIR;
  
        if PLAT_WIN32 do
            C_hack::ignore$ spec.xqt("mkdir "+spec.quote(spec.CACHE_DIR+"\\rubbish")+"& rd /Q /S " + spec.quote(spec.CACHE_DIR));
        else
            C_hack::ignore$ spec.xqt("rm -rf " + spec.quote(spec.CACHE_DIR));
        done
  
        // Make a new cache.
        Directory::mkdirs(spec.CACHE_DIR);
  
        // make the stamp file with the time of the current compiler.
        var f = fopen_output flxg_stamp;
        write(f, fmt(flxg_time, fixed (0,3)));
        f.fclose;
      done
      return spec.CLEAR_CACHE, cache_time;
    }
  
    fun cache_join (c:string, var f:string) =
    {
      //debugln$ "[cache_join] " + c + " with  " + f;
      if PLAT_WIN32 do
        if f.[1 to 3] == ":\\" do f = f.[0 to 1]+f.[2 to];
        elif f.[1] == char ":" do f = f.[0 to 1]+"\\"+f.[2 to];
        done
        if f.[0] == char "\\" do f = f.[1 to]; done
      else
        if f.[0] == char "/" do f = f.[1 to]; done
      done
        var k = Filename::join(c,f);
        //debugln$ "[cache_join] result = " + k;
        return k;
    }
  
  }