#############################################################################
##
#W  scanmtx.gi     GAP 4 packages AtlasRep and MeatAxe          Thomas Breuer
#W                                                               Frank Lübeck
##
#Y  Copyright (C)  2001,   Lehrstuhl D für Mathematik,  RWTH Aachen,  Germany
##
##  Whenever this file is changed in one of the packages
##  'atlasrep' or 'meataxe',
##  do not forget to update the corresponding file in the other package!
##
##  This file contains the implementation part of the interface routines for
##  reading and writing C-MeatAxe text and binary format,
##  and straight line programs used in the ATLAS of Group Representations.
##
##  The functions 'CMtxBinaryFFMatOrPerm' and 'FFMatOrPermCMtxBinary'
##  were contributed by Frank Lübeck.
##


############################################################################
##
#V  CMeatAxe
##
InstallValue( CMeatAxe,
    rec(
                gennames := [],
                alpha    := "abcdefghijklmnopqrstuvwxyz",
                maxnr    := 0
               ) );


#############################################################################
##
#V  FFLists
#V  FFLogLists
#V  NONNEG_INTEGERS_STRINGS
##
##  The bijection between elements in the finite field with $q$ elements and
##  the integers in the set $\{ 0, 1, \ldots, q-1 \}$ is implemented
##  via lookup tables `l1 = FFList( GF(<q>) )'
##  and `l2 = FFLogList( GF(<q>) )'.
##
##  If the field element <x> corresponds to the integer <i> then
##  the relations $<i> = `Position( FFList( GF(<q>) ), <x> )' - 1$ and
##  $<x> = `FFList( GF(<q>) )'[ <i>+1 ]$ hold.
##
##  If $q = p$ then $<i> = `IntFFE'( <x> )$ holds,
##  which is cheaper to compute than via `Position';
##  Also $<x> = <i> * `Z(p)^0'$ holds, so the lookup table need not be
##  created for large prime fields.
##
##  In order to avoid the calls to `Position' also in the case of non-prime
##  fields, the lookup list `FFLogList( GF(<q>) )' is used.
##  For $<x> = <z>^k$,
##  we have `String( <i> ) = FFLogList( GF(<q>) )[ LogFFE( <x>, <z> ) + 1 ]'.
##
InstallFlushableValue( FFLists, [] );
InstallFlushableValue( FFLogLists, [] );
InstallFlushableValue( NONNEG_INTEGERS_STRINGS, [] );


#############################################################################
##
#F  FFList( <F> )
##
##  (This program was originally written by Meinolf Geck.)
##
InstallGlobalFunction( FFList, function( F )
    local sizeF, dim, root, pow, powers, i, elms;

    sizeF:= Size( F );

    if not IsBound( FFLists[ sizeF ] ) then

      dim:= DegreeOverPrimeField( F );
      root:= PrimitiveRoot( F );
      pow:= One( root );
      powers:= [ pow ];
      for i in [ 2 .. dim ] do
        pow:= pow * root;
        powers[i]:= pow;
      od;
      elms:= Tuples( [ 0 .. Characteristic( F )-1 ], dim )
             * Reversed( powers );
      ConvertToVectorRep( elms, sizeF );
      FFLists[ sizeF ]:= elms;

    fi;

    return FFLists[ sizeF ];
end );


#############################################################################
##
#F  FFLogList( <F> )
##
InstallGlobalFunction( FFLogList, function( F )
    local sizeF, fflist, z, result, i;

    sizeF:= Size( F );

    if not IsBound( FFLogLists[ sizeF ] ) then
      fflist:= FFList( F );
      z:= Z( sizeF );
      result:= [ 0 ];
      result[ Length( fflist ) ]:= 0;
      for i in [ 2 .. Length( fflist ) ] do
        result[ LogFFE( fflist[i], z ) + 1 ]:= String( i-1 );
      od;
      FFLogLists[ sizeF ]:= result;
    fi;

    return FFLogLists[ sizeF ];
end );


#############################################################################
##
#F  IntegerStrings( <q> )
##
InstallGlobalFunction( IntegerStrings, function( q )
    local i;
    for i in [ Length( NONNEG_INTEGERS_STRINGS ) + 1 .. q ] do
      NONNEG_INTEGERS_STRINGS[i]:= String( i-1 );
    od;
    return NONNEG_INTEGERS_STRINGS;
end );


#############################################################################
##
#F  CMeatAxeFileHeaderInfo( <string> )
##
InstallGlobalFunction( "CMeatAxeFileHeaderInfo", function( string )
    local lline, header, pos, line, degree;

    # Remove a comment part if necessary.
    pos:= Position( string, '#' );
    if pos <> fail then
      string:= string{ [ 1 .. pos ] };
    fi;

    # Split the header string, and convert the entries to integers.
    header:= SplitString( string, "", " \n" );
    lline:= List( header, Int );
    if fail in lline then

      # If the header is valid then it is of new type.
      if Length( header ) = 2 and header[1] = "permutation" then

        line:= [ 12, 1,, 1 ];
        if 7 < Length( header[2] ) and header[2]{ [ 1 .. 7 ] } = "degree=" then
          degree:= Int( header[2]{ [ 8 .. Length( header[2] ) ] } );
          line[3]:= degree;
        fi;

      elif Length( header ) = 4 and header[1] = "matrix" then

        line:= [ 6 ];
        if 6 < Length( header[2] ) and header[2]{ [ 1 .. 6 ] } = "field=" then
          line[2]:= Int( header[2]{ [ 7 .. Length( header[2] ) ] } );
          if IsInt( line[2] ) and line[2] < 10 then
            line[1]:= 1;
          fi;
        fi;
        if 5 < Length( header[3] ) and header[3]{ [ 1 .. 5 ] } = "rows=" then
          line[3]:= Int( header[3]{ [ 6 .. Length( header[3] ) ] } );
        fi;
        if 5 < Length( header[4] ) and header[4]{ [ 1 .. 5 ] } = "cols=" then
          line[4]:= Int( header[4]{ [ 6 .. Length( header[4] ) ] } );
        fi;

      elif Length( header ) = 4 and header[1] = "integer"
                                and header[2] = "matrix" then

        line:= [ 8, 0 ];
        if 5 < Length( header[3] ) and header[3]{ [ 1 .. 5 ] } = "rows=" then
          line[3]:= Int( header[3]{ [ 6 .. Length( header[3] ) ] } );
        fi;
        if 5 < Length( header[4] ) and header[4]{ [ 1 .. 5 ] } = "cols=" then
          line[4]:= Int( header[4]{ [ 6 .. Length( header[4] ) ] } );
        fi;

      fi;

      if fail in line or Number( line ) <> 4 then
        Info( InfoCMeatAxe, 1,
              "corrupted (new) MeatAxe file header" );
        return fail;
      fi;

    else

      # The header is of old type, consisting of four integers.
      if Length( lline ) = 3 and lline[1] = 12 then

        # We may be dealing with permutations of a degree requiring
        # (at least) six digits, for example the header for a permutation
        # on $100000$ points can be `12     1100000     1'.
        line:= String( lline[2] );
        if line[1] = '1' then
          lline[4]:= lline[3];
          lline[3]:= Int( line{ [ 2 .. Length( line ) ] } );
          lline[2]:= 1;
        fi;

      fi;

      if Length( lline ) <> 4 then
        Info( InfoCMeatAxe, 1,
              "corrupted (old) MeatAxe file header" );
        return fail;
      fi;
      line:= lline;

    fi;

    return line;
end );


#############################################################################
##
#F  ScanMeatAxeFile( <filename>[, <q>] )
#F  ScanMeatAxeFile( <string>[, <q>], "string" )
##
InstallGlobalFunction( ScanMeatAxeFile, function( arg )
    local PermListWithTest,
          filename,
          file,
          line,
          q,
          mode,
          degree,
          result,
          string,
          headlen,
          pos,
          pos2,
          offset,
          j,
          imgs,
          i,
          Fsize,
          F,
          nrows,
          fflist,
          newline,
          ncols,
          one,
          len;

    PermListWithTest:= function( list )
      local perm;

      perm:= PermList( list );
      if perm = fail then
        if ForAny( list, x -> not IsInt( x ) ) then
          Info( InfoCMeatAxe, 1,
                "non-permutation in file, contains ",
                Filtered( list, x -> not IsInt( x ) ) );
        fi;
        Info( InfoCMeatAxe, 1,
              "non-permutation in file,\n#I  ",
              Difference( [ 1 .. degree ], list ), " missing, ",
              List( Filtered( Collected( list ),
                    x -> x[2] <> 1 ), x -> x[1] ), " duplicate" );
      fi;
      return perm;
    end;

    if    Length( arg ) = 1
       or ( Length( arg ) = 2 and IsPosInt( arg[2] ) ) then

      filename:= arg[1];

      # Do we want to read the file *fast* (but using more space)?
      AGR.InfoRead( "#I  reading `", filename, "' started\n" );
      if UserPreference( "AtlasRep", "HowToReadMeatAxeTextFiles" ) = "fast"
         then
        string:= AGR.StringFile( filename );
        if Length( arg ) = 1 then
          return ScanMeatAxeFile( string, "string" );
        else
          return ScanMeatAxeFile( string, arg[2], "string" );
        fi;
      fi;

      file:= InputTextFile( filename );
      if file = fail then
        Error( "cannot create input stream for file <filename>" );
      fi;
      line:= ReadLine( file );
      if line = fail then
        Info( InfoCMeatAxe, 1, "no first line exists" );
        CloseStream( file );
        return fail;
      fi;
      while not '\n' in line do
        Append( line, ReadLine( file ) );
      od;
      if Length( arg ) = 2 then
        q:= arg[2];
      fi;

    elif    ( Length( arg ) = 2 and IsString( arg[1] ) and arg[2] = "string" )
         or ( Length( arg ) = 3 and IsString( arg[1] ) and IsPosInt( arg[2] )
                                and arg[3] = "string" ) then

      # Cut out the header line.
      string:= arg[1];
      headlen:= Position( string, '\n' );
      if headlen = fail then
        return fail;
      fi;
      line:= string{ [ 1 .. headlen-1 ] };
      string:= ShallowCopy( string );
      string{ [ 1 .. headlen ] }:= ListWithIdenticalEntries( headlen, ' ' );
      if Length( arg ) = 3 then
        q:= arg[2];
      fi;

    else
      Error( "usage: ScanMeatAxeFile( <filename>[, <q>] )" );
    fi;

    # Interpret the first line as file header.
    # From the first line, determine whether a matrix or a permutation
    # is to be read.
    line:= CMeatAxeFileHeaderInfo( line );
    if line = fail then
      Info( InfoCMeatAxe, 1, "corrupted file header" );
      if not IsBound( string ) then
        CloseStream( file );
      fi;
      return fail;
    fi;

    mode:= line[1];

    if mode in [ 2, 12, 8 ] then

      # a permutation, to be converted to a matrix,
      # or a list of permutations,
      # or an integer matrix
      if not IsBound( string ) then
        string:= AGR.StringFile( filename );

        # Omit the header line.
        headlen:= Position( string, '\n' );
        string{ [ 1 .. headlen ] }:= ListWithIdenticalEntries( headlen, ' ' );

        CloseStream( file );
        AGR.InfoRead( "#I  reading `", filename, "' done\n" );
      fi;

      # Remove comment parts
      # (not really clever, but we actually do not expect comments).
      pos:= Position( string, '#' );
      while pos <> fail do
        pos2:= Position( string, '\n', pos );
        if pos2 = fail then
          pos2:= Length( string ) + 1;
        fi;
        string:= Concatenation( string{ [ 1 .. pos-1 ] },
                                string{ [ pos2 .. Length( string ) ] } );
        pos:= Position( string, '#' );
      od;

      # Split the line into substrings representing integers.
      # (Admittedly, the code looks ugly, but simply calling `SplitString'
      # and `Int' is slower, and calling `SplitString' and `EvalString'
      # would be even slower than that.)
      if 12 < headlen then
        string{ [ 1 .. 13 ] }:= "SMFSTRING:=[ ";
      else
        string:= Concatenation( "SMFSTRING:=[ ", string );
      fi;
      NormalizeWhitespace( string );
      pos:= Position( string, ' ' );
      pos:= Position( string, ' ', pos );
      while pos <> fail do
        string[ pos ]:= ',';
        pos:= Position( string, ' ', pos );
      od;
      Append( string, "];" );
      file:= InputTextString( string );
      Read( file );
      string:= SMFSTRING;
      SMFSTRING:= "";

      if mode = 12 then

        # mode = 12:
        # a list of permutations (in free format)
        degree:= line[3];
        result:= [];
        if Length( string ) <> degree * line[4] then
          Info( InfoCMeatAxe, 1, "corrupted file, wrong number of entries" );
          return fail;
        fi;
        if line[4] = 1 then
          # This is the usual case (avoid duplicating the list).
          result[1]:= PermListWithTest( string );
        else
          offset:= 0;
          for j in [ 1 .. line[4] ] do
            result[j]:=
                PermListWithTest( string{ [ offset+1 .. offset+degree ] } );
            offset:= offset + degree;
          od;
        fi;
        if fail in result then
          return fail;
        fi;

      elif mode = 2 then

        # mode = 2:
        # a permutation, to be converted to a matrix
        # (Note that we cannot leave the task to `PermutationMat'
        # because we admit also non-square results.)
        nrows:= line[3];
        if Length( string ) <> nrows then
          Info( InfoCMeatAxe, 1, "corrupted file" );
          return fail;
        fi;
        F:= GF( line[2] );
        result:= NullMat( nrows, line[4], F );
        one:= One( F );
        for i in [ 1 .. nrows ] do
          result[i][ string[i] ]:= one;
        od;

        # Convert the matrix to the compressed representation.
        MakeImmutable( result );
        if not IsBound( q ) then
          q:= line[2];
        fi;
        ConvertToMatrixRep( result, q );

      else

        # mode = 8:
        # an integer matrix
        nrows:= line[3];
        ncols:= line[4];
        if Length( string ) <> nrows * ncols then
          Info( InfoCMeatAxe, 1, "corrupted file" );
          return fail;
        fi;
        result:= NullMat( nrows, ncols );
        pos:= 1;
        for i in [ 1 .. nrows ] do
          for j in [ 1 .. ncols ] do
            result[i][j]:= string[ pos ];
            pos:= pos + 1;
          od;
        od;

      fi;

    elif mode = 1 then

      # a matrix, in fixed format
      Fsize:= line[2];
      if Fsize > 10 then
        Info( InfoCMeatAxe, 1, "too large field for mode 1 matrix" );
        return fail;
#T Extend the scope of mode 1, as proposed by Richard:
#T Allow A .. Z for 10 .. 35 and a .. z for -26 .. -1.
      fi;
      F:= GF( Fsize );
      fflist:= FFList( F );
      fflist:= fflist{ Concatenation( ListWithIdenticalEntries( 47, 1 ),
                                      [ 1 .. Length( fflist ) ] ) };
      nrows:= line[3];
      ncols:= line[4];
      result:= [];

      if IsBound( string ) then

        # Remove comment parts
        # (not really clever, but we actually do not expect comments).
        pos:= Position( string, '#' );
        while pos <> fail do
          pos2:= Position( string, '\n', pos );
          if pos2 = fail then
            pos2:= Length( string ) + 1;
          fi;
          string:= Concatenation( string{ [ 1 .. pos-1 ] },
                                  string{ [ pos2 .. Length( string ) ] } );
          pos:= Position( string, '#' );
        od;

        # The string is available in GAP.
        RemoveCharacters( string, " \n" );
        if Length( string ) <> nrows * ncols then
          Info( InfoCMeatAxe, 1, "string length does not fit" );
          return fail;
        fi;
        pos:= 0;
        for i in [ 1 .. nrows ] do
          result[i]:= fflist{ INTLIST_STRING(
                                 string{ [ pos+1 .. pos+ncols ] }, -1 ) };
          pos:= pos + ncols;
        od;

      else

        # The file is read line by line (for space reasons).
        line:= "";
        len:= 0;
        for i in [ 1 .. nrows ] do

          # Read enough lines from the file to fill the `i'-th row.
          while len < ncols do
            newline:= ReadLine( file );
            if newline = fail then
              Info( InfoCMeatAxe, 1, "corrupted file" );
              CloseStream( file );
              return fail;
            fi;
while not '\n' in newline do
  Append( newline, ReadLine( file ) );
od;

            # Remove comment parts.
            pos:= Position( newline, '#' );
            if pos <> fail then
              newline:= newline{ [ 1 .. pos-1 ] };
            fi;

            RemoveCharacters( newline, " \n" );
            Append( line, newline );
            len:= len + Length( newline );
          od;
          result[i] := fflist{ INTLIST_STRING( line{ [ 1 .. ncols ] }, -1 ) };
          line:= line{ [ ncols + 1 .. len ] };
          len:= len - ncols;

        od;

        # Close the stream.
        CloseStream( file );
        AGR.InfoRead( "#I  reading `", filename, "' done\n" );

      fi;

      # Convert further.
      MakeImmutable( result );
      if not IsBound( q ) then
        q:= Fsize;
      fi;
      ConvertToMatrixRep( result, q );

    elif mode in [ 3, 4, 5, 6 ] then

      # a matrix, in various free formats
      # (Prime fields could be treated in a special way,
      # without calling `FFList'; but this seems to yield no speedup,
      # except in exotic cases where `FFList' itself is the most expensive
      # part of the computation.)
      Fsize:= line[2];
      F:= GF( Fsize );
      nrows:= line[3];
      ncols:= line[4];
      result:= [];
      if not IsBound( q ) then
        q:= Fsize;
      fi;

      if mode = 5 then
        one:= One( F );
      else
        fflist:= FFList( F );
      fi;

      # The case of a string that is available in GAP is dealt with
      # in parallel with the case of a file that is read line by line
      # (for space reasons).
      if IsBound( string ) then

        # Remove comment parts if applicable.
        # (not really clever, but we actually do not expect comments).
        while '#' in string do
          pos:= Position( string, '#' );
          pos2:= Position( string, '\n', pos );
          if pos2 = fail then
            pos2:= Length( string ) + 1;
          fi;
          string:= Concatenation( string{ [ 1 .. pos-1 ] },
                                  string{ [ pos2 .. Length( string ) ] } );
        od;

        # Split the string into substrings representing numbers.
        # (Admittedly, the code looks ugly, but simply calling `SplitString'
        # and `Int' is slower, and calling `SplitString' and `EvalString'
        # would be even slower than that.)
        if 12 < headlen then
          string{ [ 1 .. 13 ] }:= "SMFSTRING:=[ ";
        else
          string:= Concatenation( "SMFSTRING:=[ ", string );
        fi;
        NormalizeWhitespace( string );
        pos:= Position( string, ' ' );
        pos:= Position( string, ' ', pos );
        while pos <> fail do
          string[ pos ]:= ',';
          pos:= Position( string, ' ', pos );
        od;
        Append( string, "];" );
        file:= InputTextString( string );
        Read( file );
        string:= SMFSTRING;
        SMFSTRING:= "";
        line:= string;

        if Length( line ) <> nrows * ncols then
          Info( InfoCMeatAxe, 1, "corrupted file" );
          return fail;
        fi;

        pos:= 0;
        for i in [ 1 .. nrows ] do
          if mode = 5 then
            # an integer matrix (in free format), to be reduced mod a prime
            result[i]:= ( line{ [ pos+1 .. pos+ncols ] } + 1 ) * one;
          else
            result[i]:= fflist{ line{ [ pos+1 .. pos+ncols ] } + 1 };
          fi;
          pos:= pos + ncols;
        od;

      else

        # The file is read line by line (for space reasons).
        line:= "";
        len:= 0;

        for i in [ 1 .. nrows ] do

          # Read enough lines from the file to fill the `i'-th row.
          while len < ncols do
            newline:= ReadLine( file );
            if newline = fail then
              Info( InfoCMeatAxe, 1, "corrupted file" );
              CloseStream( file );
              return fail;
            fi;
while not '\n' in newline do
  Append( newline, ReadLine( file ) );
od;

            # Remove comment parts.
            pos:= Position( newline, '#' );
            if pos <> fail then
              newline:= newline{ [ 1 .. pos-1 ] };
            fi;

            newline:= List( SplitString( newline, "", " \n" ), Int );
            Append( line, newline );
            len:= len + Length( newline );
          od;

          if mode = 5 then
            # an integer matrix (in free format), to be reduced mod a prime
            result[i]:= ( line{ [ 1 .. ncols ] } + 1 ) * one;
          else
            result[i]:= fflist{ line{ [ 1 .. ncols ] } + 1 };
          fi;
          line:= line{ [ ncols + 1 .. len ] };
          len:= len - ncols;

        od;

        # Close the stream.
        CloseStream( file );
        AGR.InfoRead( "#I  reading `", filename, "' done\n" );

      fi;

      # Convert further.
      MakeImmutable( result );
      ConvertToMatrixRep( result, q );

    else
      Info( InfoCMeatAxe, 1, "unknown mode" );
      return fail;
    fi;

    return result;
end );


#############################################################################
##
#M  MeatAxeString( <mat>, <q> )
##
InstallMethod( MeatAxeString,
    [ "IsTable and IsFFECollColl", "IsPosInt" ],
    function( mat, q )
    local nrows,     # number of rows of `mat'
          ncols,     # number of columns of `mat'
          one,       # identity element of the field of matrix entries
          zero,      # zero element of the field of matrix entries
          perm,      # list of perm. images if `mat' is a perm. matrix
          i,         # loop over the rows of `mat'
          noone,     # no `one' found yet in the current row
          row,       # one row of `mat'
          j,         # loop over the columns of `mat'
          mode,      # mode of the MeatAxe string (first header entry)
          header,    # mode of the header
          values,
          linelen,
          nol,
          tail,      #
          fflist,    #
          len,
          str,       # MeatAxe string, result
          k,
          N,
          d,
          ffloglist, #
          z;         #

    # Check that `mat' is rectangular.
    if not IsMatrixObj( mat ) then
      Error( "<mat> must be a matrix" );
    fi;
    nrows:= NumberRows( mat );
    ncols:= NumberColumns( mat );

    # Check that `q' and `mat' are compatible.
    if not IsPrimePowerInt( q ) or ( q mod Characteristic( mat ) <> 0 ) then
      Error( "<q> and the characteristic of <mat> are incompatible" );
    fi;

    one:= One( mat[1,1] );
    zero:= Zero( one );
    perm:= fail;
    if UserPreference( "AtlasRep", "WriteMeatAxeFilesOfMode2" ) = true then
      # If the matrix is a ``generalized permutation matrix''
      # then construct a string of MeatAxe mode 2.
      perm:= [];
      for i in [ 1 .. nrows ] do
        noone:= true;
        row:= mat[i];
        if IsZero( row ) then
          perm:= fail;
        else
          for j in [ 1 .. ncols ] do
            if row[j] = one then
#T deal with mat[i,j] etc.
              if noone and not j in perm then
                perm[i]:= j;
                noone:= false;
              else
                perm:= fail;
                break;
              fi;
            elif row[j] <> zero then
              perm:= fail;
              break;
            fi;
          od;
        fi;
        if perm = fail then
          break;
        fi;
      od;
    fi;

    # Start with the header line.
    # We try to keep the files as small as possible by using the (old)
    # mode `1' whenever possible.
    if perm <> fail then
      mode:= "2";
    elif q < 10 then
      mode:= "1";
    else
      mode:= "6";
    fi;

    header:= UserPreference( "AtlasRep", "WriteHeaderFormatOfMeatAxeFiles" );
    if   header= "numeric" then
      header:= Concatenation( mode, " ", String( q ), " ",
                   String( nrows ), " ", String( ncols ), "\n" );
    elif header= "numeric (fixed)" then
      header:= Concatenation( String( mode, 6 ), " ", String( q, 5 ), " ",
                   String( nrows, 5 ), " ", String( ncols, 5 ), "\n" );
    elif header= "textual" then
      header:= Concatenation( "matrix field=", String( q ), " rows=",
                   String( nrows ), " cols=", String( ncols ), "\n" );
    else
      Error( "the user preference 'WriteHeaderFormatOfMeatAxeFiles' must ",
             "be one of \"numeric\", \"numeric (fixed)\", \"textual\"" );
    fi;

    # Add the matrix entries.
    if mode = "1" then

      # Set the parameters for filling lines in the fixed format
      values:= "0123456789";
      linelen:= 80;
      nol:= Int( ncols / linelen );
      tail:= [ nol * linelen + 1 .. ncols ];
      fflist:= FFList( GF(q) );

      # Compute the length of the string.
      len:= Length( header )
            + nrows * ( ncols + 1 + Int( ( ncols - 1 ) / linelen ) );
      str:= EmptyString( len );
      Append( str, header );

      for row in mat do
        i:= 1;
        for j in [ 0 .. nol-1 ] do
          for k in [ 1 .. linelen ] do
            Add( str, values[ Position( fflist, row[i] ) ] );
#T deal with mat[i,j]
            i:= i + 1;
          od;
          Add( str, '\n' );
        od;
        if not IsEmpty( tail ) then
          for i in tail do
            Add( str, values[ Position( fflist, row[i] ) ] );
          od;
          Add( str, '\n' );
        fi;
      od;

    elif mode = "2" then

      # Compute the length of the string.
      N:= Length( perm );
      k:= LogInt( N, 10 );
      d:= 0;
      for i in [ 1 .. k ] do
        d:= d * 10;
        d:= d + i;
      od;
      len:= Length( header )
            + ( k + 2 ) * N - 9 * d;
      str:= EmptyString( len );
      Append( str, header );

      for i in perm do
        Append( str, String( i ) );
        Add( str, '\n' );
      od;

    else

      # free format
      # The length of the result depends on the matrix entries,
      # thus we start with an estimate of the length.
      len:= Length( header )
            + nrows * ncols * Int( ( LogInt( q, 10 ) + 4 ) / 2 );
      str:= EmptyString( len );
      Append( str, header );

      if IsPrimeInt( q ) then
        # Avoid the call to `FFList', and expensive `Position' calls.
        # Also store the strings once;
        # this cache avoids garbage collections and thus saves time
        # if the matrix is not too small, compared to the field.
        values:= IntegerStrings( q );
        for row in mat do
          for i in row do
            Append( str, values[ IntFFE( i ) + 1 ] );
            Add( str, '\n' );
          od;
        od;
      else
        # Avoid expensive `Position' calls for the result of `FFList'.
        ffloglist:= FFLogList( GF(q) );
        z:= Z(q);
        for row in mat do
          for i in row do
            if i = zero then
              Append( str, "0\n" );
            else
              Append( str, ffloglist[ LogFFE( i, z ) + 1 ] );
              Add( str, '\n' );
            fi;
          od;
        od;
      fi;

      ShrinkAllocationString( str );

    fi;

    # Return the result.
    return str;
    end );


#############################################################################
##
#M  MeatAxeString( <perms>, <degree> )
##
InstallMethod( MeatAxeString,
    [ "IsPermCollection and IsList", "IsPosInt" ],
    function( perms, degree )
    local header, k, d, len, str, perm, i;

    # Start with the header line.
    header:= UserPreference( "AtlasRep", "WriteHeaderFormatOfMeatAxeFiles" );
    if   header= "numeric" then
      header:= Concatenation( "12 1 ", String( degree ), " ",
                   String( Length( perms ) ), "\n" );
    elif header= "numeric (fixed)" then
      header:= Concatenation( "    12     1 ", String( degree, 5 ), " ",
                   String( Length( perms ), 5 ), "\n" );
    elif header= "textual" then
      if Length( perms ) <> 1 then
        Error( "<perms> must have length 1 if the user preference ",
               "\"WriteHeaderFormatOfMeatAxeFiles\" has the value ",
               "\"textual\"" );
      fi;
      header:= Concatenation( "permutation degree=", String( degree ), "\n" );
    else
      Error( "the user preference 'WriteHeaderFormatOfMeatAxeFiles' must ",
             "be one of \"numeric\", \"numeric (fixed)\", \"textual\"" );
    fi;

    # Compute the length of the string.
    k:= LogInt( degree, 10 );
    d:= 0;
    for i in [ 1 .. k ] do
      d:= d * 10;
      d:= d + i;
    od;
    len:= Length( header )
          + ( k + 2 ) * degree - 9 * d;
    str:= EmptyString( len );
    Append( str, header );

    # Add the images.
    for perm in perms do
      for i in [ 1 .. degree ] do
        Append( str, String( i ^ perm ) );
        Add( str, '\n' );
      od;
    od;

    # Return the result.
    return str;
    end );


#############################################################################
##
#M  MeatAxeString( <perm>, <q>, <dims> )
##
InstallMethod( MeatAxeString,
    [ "IsPerm", "IsPosInt", "IsList" ],
    function( perm, q, dims )
    local header, str, i;

    # Start with the header line.
    # (The mode is `2': a permutation, to be converted to a matrix.)
    header:= UserPreference( "AtlasRep", "WriteHeaderFormatOfMeatAxeFiles" );
    if   header = "numeric" then
      str:= "2 ";                         # mode,
      Append( str, String( q ) );         # field size,
      Append( str, " " );
      Append( str, String( dims[1] ) );   # number of rows,
      Append( str, " " );
      Append( str, String( dims[2] ) );   # number of columns
      Append( str, "\n" );
    elif header = "numeric (fixed)" then
      str:= "     2 ";
      Append( str, String( q, 5 ) );
      Append( str, " " );
      Append( str, String( dims[1], 5 ) );
      Append( str, " " );
      Append( str, String( dims[2], 5 ) );
      Append( str, "\n" );
    elif header = "textual" then
#T does zcv support this at all?
      str:= "permutation degree=";
      Append( str, String( q ) );         # field size,
      Append( str, "\n" );
    else
      Error( "the user preference 'WriteHeaderFormatOfMeatAxeFiles' must ",
             "be one of \"numeric\", \"numeric (fixed)\", \"textual\"" );
    fi;

    # Add the images.
    for i in [ 1 .. dims[1] ] do
      Append( str, String( i ^ perm ) );
      Add( str, '\n' );
    od;

    # Return the result.
    return str;
    end );


#############################################################################
##
#M  MeatAxeString( <intmat> )
##
##  This is supported only for integer matrices
##  and only with the new header format.
##
InstallMethod( MeatAxeString,
    [ "IsTable and IsCyclotomicCollColl" ],
    function( intmat )
    local str, row, entry;

    # Start with the header line.
    str:= "integer matrix rows=";
    Append( str, String( Length( intmat ) ) );
    Append( str, " cols=" );
    Append( str, String( Length( intmat[1] ) ) );
    Append( str, "\n" );

    for row in intmat do
      for entry in row do
        if not IsInt( entry ) then
          Error( "in characteristic zero, ",
                 "only integer matrices are supported by MeatAxeString" );
        fi;
        Append( str, String( entry ) );
        Append( str, " " );
      od;
      Add( str, '\n' );
    od;

    # Return the result.
    return str;
    end );


#############################################################################
##
#F  CMtxBinaryFFMatOrPerm( <mat>, <q>, <outfile> )
#F  CMtxBinaryFFMatOrPerm( <perm>, <deg>, <outfile>[, <base>] )
##
InstallGlobalFunction( CMtxBinaryFFMatOrPerm, function( arg )
  local mat, q, outfile, res, qr, imgs, i, f, a, epb, qpwrs, ffloglist, z,
        len, ind, row, x;

  if Length( arg ) < 3 or 4 < Length( arg ) then
    Error( "usage: ",
           "CMtxBinaryFFMatOrPerm( <perm>, <deg>, <outfile>[, <base>] )" );
  fi;
  mat:= arg[1];
  q:= arg[2];
  outfile:= arg[3];

  if IsPerm( mat ) then
    res:= [ 255, 255, 255, 255 ];
    qr:= q;
    for i in [ 1 .. 4 ] do
      Add( res, RemInt( qr, 256 ) );
      qr:= QuoInt( qr, 256 );
    od;
    Append( res, [ 1, 0, 0, 0 ] );
    imgs:= OnTuples( [ 1 .. q ], mat );
    if Length( arg ) = 4 and arg[4] = 0 then
      imgs:= imgs - 1;
    fi;
    for qr in imgs do
      for i in [ 1 .. 4 ] do
        Add( res, RemInt( qr, 256 ) );
        qr:= QuoInt( qr, 256 );
      od;
    od;
    f:= OutputTextFile( outfile, false );
    WriteAll( f, STRING_SINTLIST( res ) );
  elif q <= 256 then
    # ff elts per byte
    a := q;
    epb := 0;
    while a<=256 do
      a := a*q;
      epb := epb+1;
    od;
    # q-powers
    qpwrs := List([epb-1,epb-2..0], i-> q^i);
    # open outfile
    f := OutputTextFile(outfile, false);
    # header is 12 = 3x4 bytes, (field size, nrrows, nrcols),
    # each  number in 256-adic decomposition, least significant first
    res := [];
    for a in [q,Length(mat),Length(mat[1])] do
      qr := [a,0];
      for i in [1..4] do
        qr := QuotientRemainder(qr[1],256);
        Add(res,qr[2]);
      od;
    od;
    WriteAll(f, STRING_SINTLIST(res));
    # now the data, pack each epb entries in one byte, weighted with qpwrs
    ffloglist:= List(FFLogList( GF(q) ), Int);
    z:= Z(q);
    for row in mat do
      len := QuoInt(Length(row), epb);
      if len * epb < Length(row) then
        len := len+1;
      fi;
      res := 0*[1..len];
      ind := [1,1];
      for x in row do
        if not IsZero(x) then
          a := ffloglist[LogFFE(x, z) + 1];
          res[ind[1]] := res[ind[1]] + a*qpwrs[ind[2]];
        fi;
        if ind[2] = epb then
          ind := [ind[1]+1, 1];
        else
          ind[2] := ind[2]+1;
        fi;
      od;
      WriteAll(f, STRING_SINTLIST(res));
    od;
  else
    Error( "<q> must be at most 256" );
  fi;
  CloseStream(f);
end );


#############################################################################
##
#F  FFMatOrPermCMtxBinary( <fname> )
##
InstallGlobalFunction( FFMatOrPermCMtxBinary, function( fname )
  local f, head, v, q, deg, bytes, list, j, zero, i, res, nrows, ncols, a,
        epb, lenrow, fflist, poslist, eltsbyte, row;
  # open file and read first 12 bytes as header
  # header is 12 = 3x4 bytes,
  # (in the matrix case field size, nrrows, nrcols;
  # in the permutation case -1, degree, 1),
  # each  number in 256-adic decomposition, least significant first
  f := InputTextFile(fname);
  if f = fail then
    Info( InfoCMeatAxe, 1,
          "cannot open ", fname );
    return fail;
  fi;
  head := ReadAll(f, 12);
  if head = fail or Length( head ) < 12 then
    Info( InfoCMeatAxe, 1,
          "file too short: ", fname );
    CloseStream( f );
    return fail;
  fi;
  head := INTLIST_STRING(head, 1);
  v := [1, 256, 256^2, 256^3];
  q := v * head{[1..4]};
  if q = 4294967295 then
    # permutation
    deg:= v * head{ [ 5 .. 8 ] };
    bytes:= INTLIST_STRING( ReadAll( f ), 1 );
    CloseStream( f );
    list:= [];
    j:= 1;
    zero:= false;
    for i in [ 1 .. deg ] do
      list[i]:= v * bytes{ [ j .. j+3 ] };
      if list[i] = 0 then
        zero:= true;
      fi;
      j:= j + 4;
    od;
    if zero then
      # format used in C-MeatAxe 2.4 (zero-based): add 1 to all entries
      for i in [ 1 .. deg ] do
        list[i]:= list[i] + 1;
      od;
    fi;
    res:= PermList( list );
    # ugly hack:
    # several of the data files on the server are stored in a wrong format.
    if res = fail then
      v:= [ 256^3, 256^2, 256, 1 ];
      j:= 1;
      zero:= false;
      for i in [ 1 .. deg ] do
        list[i]:= v * bytes{ [ j .. j+3 ] };
        if list[i] = 0 then
          zero:= true;
        fi;
        j:= j + 4;
      od;
      if zero then
        # format used in C-MeatAxe 2.4 (zero-based): add 1 to all entries
        for i in [ 1 .. deg ] do
          list[i]:= list[i] + 1;
        od;
      fi;
      res:= PermList( list );
      if res = fail then
        Info( InfoCMeatAxe, 1,
              "not a permutation: ", fname );
      fi;
    fi;
  else
    # matrix
    nrows := v * head{[5..8]};
    ncols := v * head{[9..12]};
    # ff elts per byte
    a := q;
    epb := 0;
    while a<=256 do
      a := a*q;
      epb := epb+1;
    od;
    # length of row in bytes
    lenrow := QuoInt(ncols, epb);
    if lenrow*epb < ncols then
      lenrow := lenrow+1;
    fi;
    # map from bytes to element lists
    fflist := FFList(GF(q));
    poslist := Cartesian(List([1..epb], i-> [0..q-1]));
    eltsbyte := List(poslist, a-> fflist{a+1});
    # now read line by line
    res := [];
    for i in [1..nrows] do
      bytes := ReadAll(f, lenrow);
      bytes := INTLIST_STRING(bytes, 1);
      row := Concatenation(eltsbyte{bytes+1});
      while Length(row) > ncols do
        Unbind(row[Length(row)]);
      od;
      ConvertToVectorRep(row, q);
      Add(res, row);
    od;
    CloseStream(f);
    ConvertToMatrixRep(res);
  fi;
  return res;
end );


#############################################################################
##
#F  ScanStraightLineProgramOrDecision( <strdata>, <mode> )
##
BindGlobal( "ScanStraightLineProgramOrDecision", function( strdata, mode )
    local data, echo, line, pos, labels, nrinputs, i, lines, output, a, b, c,
          outputs, result, search;

    data:= [];
    echo:= [];
    for line in SplitString( strdata, "", "\n" ) do

      # Omit empty lines and comment parts.
      if   4 < Length( line ) and line{ [ 1 .. 4 ] } = "echo" then
        Add( echo, line );
      elif not IsEmpty( line ) then
        pos:= Position( line, '#' );
        if pos <> fail then
          line:= line{ [ 1 .. pos-1 ] };
        fi;
        line:= SplitString( line, "", " \n" );
        if not IsEmpty( line ) then
          Add( data, line );
        fi;
      fi;

    od;

    # Determine the labels that occur.
    labels:= [];
    nrinputs:= 0;
    if IsEmpty( data ) or data[1][1] <> "inp" then

      # There is no `inp' line.
      # The default input is given by the labels `1' and `2'.
      labels:=[ "1", "2" ];
      nrinputs:= 2;

    fi;
    for line in data do
      if line[1] = "inp" then
        if Length( line ) = 2 and Int( line[2] ) <> fail then
          Append( labels, List( [ 1 .. Int( line[2] ) ], String ) );
          nrinputs:= nrinputs + Int( line[2] );
        elif Int( line[2] ) = Length( line ) - 2 then
          Append( labels, line{ [ 3 .. Length( line ) ] } );
          nrinputs:= nrinputs + Length( line ) - 2;
        else
          Info( InfoCMeatAxe, 1, "corrupted line `", line, "'" );
          return fail;
        fi;
      elif not ( line[1] in [ "cjr", "chor" ] ) then
        i:= Length( line );
        if not line[i] in labels then
          Add( labels, line[i] );
        fi;
      fi;
    od;

    # Translate the lines.
    lines:= [];
    output:= [];
    c:= 0;
    for line in data do
      if line[1] = "oup" or line[1] = "op" then

        Add( output, line );

      elif line[1] <> "inp" then

        if   line[1] = "mu" and Length( line ) = 4 then
          a:= Position( labels, line[2] );
          b:= Position( labels, line[3] );
          c:= Position( labels, line[4] );
          Add( lines, [ [ a, 1, b, 1 ], c ] );
        elif line[1] = "iv" and Length( line ) = 3 then
          a:= Position( labels, line[2] );
          b:= Position( labels, line[3] );
          Add( lines, [ [ a, -1 ], b ] );
        elif line[1] = "pwr" and Length( line ) = 4 then
          a:= Int( line[2] );
          b:= Position( labels, line[3] );
          c:= Position( labels, line[4] );
          Add( lines, [ [ b, a ], c ] );
        elif line[1] = "cjr" and Length( line ) = 3 then
          a:= Position( labels, line[2] );
          b:= Position( labels, line[3] );
          Add( lines, [ [ b, -1, a, 1, b, 1 ], a ] );
        elif line[1] = "cp" and Length( line ) = 3 then
          a:= Position( labels, line[2] );
          b:= Position( labels, line[3] );
          Add( lines, [ [ a, 1 ], b ] );
        elif line[1] = "cj" and Length( line ) = 4 then
          a:= Position( labels, line[2] );
          b:= Position( labels, line[3] );
          c:= Position( labels, line[4] );
          Add( lines, [ [ b, -1, a, 1, b, 1 ], c ] );
        elif line[1] = "com" and Length( line ) = 4 then
          a:= Position( labels, line[2] );
          b:= Position( labels, line[3] );
          c:= Position( labels, line[4] );
          Add( lines, [ [ a, -1, b, -1, a, 1, b, 1 ], c ] );
# improve: three multiplications and one inversion suffice!
        elif line[1] = "chor" and Length( line ) = 3 then
          if mode = "program" then
            Info( InfoCMeatAxe, 1,
                  "no \"chor\" lines allowed in straight line programs" );
            return fail;
          fi;
          a:= Position( labels, line[2] );
          b:= Int( line[3] );
          Add( lines, [ "Order", a, b ] );
        else
          Info( InfoCMeatAxe, 1, "strange line `", line, "'" );
          return fail;
        fi;
        if a = fail or b = fail or c = fail then
          Info( InfoCMeatAxe, 1, "line `", line, "' uses undefined label" );
          return fail;
        fi;

      fi;
    od;

    # Specify the output.
    result:= rec();
    if IsEmpty( output ) and mode = "program" then

      # The default output is given by the labels `1' and `2'.
      # Note that this is allowed only if these labels really occur.
      if IsSubset( labels, [ "1", "2" ] ) then
        Add( lines, [ [ Position( labels, "1" ), 1 ],
                      [ Position( labels, "2" ), 1 ] ] );
      else
        Info( InfoCMeatAxe, 1, "missing `oup' statement" );
        return fail;
      fi;

    elif mode = "program" then

      # The `oup' lines list the output labels.
      outputs:= [];
      for line in output do
        if Length( line ) = 2 and Int( line[2] ) <> fail then
          Append( outputs, List( [ 1 .. Int( line[2] ) ],
                          x -> [ Position( labels, String( x ) ), 1 ] ) );
        elif Length( line ) = Int( line[2] ) + 2 then
          Append( outputs, List( line{ [ 3 .. Length( line ) ] },
                          x -> [ Position( labels, x ), 1 ] ) );
        else
          Info( InfoCMeatAxe, 1, "corrupted line `", line, "'" );
          return fail;
        fi;
      od;
      if ForAny( outputs, pair -> pair[1] = fail ) then
        Info( InfoCMeatAxe, 1, "undefined label used in output line(s)" );
        return fail;
      fi;
      Add( lines, outputs );

      # The straight line program is thought for computing
      # class representatives,
      # and the bijection between the output labels and the class names
      # is given by the `echo' lines.
      # For the line `oup <l> <b1> <b2> ... <bl>',
      # there must be the `echo' line `Classes <n1> <n2> ... <nl>'.
      echo:= List( echo, line -> SplitString( line, "", "\" \n" ) );
      while     not IsEmpty( echo )
            and LowercaseString( echo[1][2] ) <> "classes" do
        echo:= echo{ [ 2 .. Length( echo ) ] };
      od;

      if not IsEmpty( echo ) then

        # Check that the `echo' lines fit to the `oup' lines.
        echo:= List( echo, x -> Filtered( x,
                 y -> y <> "echo" and LowercaseString( y ) <> "classes" ) );
        outputs:= Concatenation( echo );
        output:= Sum( List( output, x -> Int( x[2] ) ), 0 );
        if Length( outputs ) < output then
          Info( InfoCMeatAxe, 1,
                "`oup' and `echo' lines not compatible" );
          return fail;
        fi;
        outputs:= outputs{ [ 1 .. output ] };
        result.outputs:= outputs;

      fi;

    fi;

    # Construct and return the result.
    if mode = "program" then
      result.program:= StraightLineProgramNC( lines, nrinputs );
    else
      result.program:= StraightLineDecisionNC( lines, nrinputs );
    fi;
    return result;
end );


#############################################################################
##
#F  ScanStraightLineDecision( <string> )
##
InstallGlobalFunction( ScanStraightLineDecision,
    string -> ScanStraightLineProgramOrDecision( string, "decision" ) );


#############################################################################
##
#F  ScanStraightLineProgram( <filename> )
#F  ScanStraightLineProgram( <string>, "string" )
##
InstallGlobalFunction( ScanStraightLineProgram, function( arg )
    local filename, strdata;

    if   Length( arg ) = 1 and arg[1] = fail then
      # This is used to simplify other programs.
      return fail;
    elif Length( arg ) = 1 and IsString( arg[1] ) then
      # Read the data.
      filename:= arg[1];
      strdata:= AGR.StringFile( filename );
      if strdata = fail then
        Error( "cannot read file <filename>" );
      fi;
    elif  Length( arg ) = 2 and IsString( arg[1] ) and arg[2] = "string" then
      strdata:= arg[1];
    else
      Error( "usage: ScanStraightLineProgram( <filename>[, \"string\"] )" );
    fi;

    return ScanStraightLineProgramOrDecision( strdata, "program" );
end );


#############################################################################
##
#F  AtlasStringOfProgram( <prog>[, <outputnames>[, <avoidslots>]] )
#F  AtlasStringOfProgram( <prog>[, <format>[, <avoidslots>]] )
##
##  <avoidslots> refers only to the result list.
##
InstallGlobalFunction( AtlasStringOfProgram, function( arg )
    local format,         # "ATLAS" or "mtx"
          avoidslots,     # optional third argument
          prog,           # straight line program, first argument
          outputnames,    # list of strings, optional second argument
          resused,        # maximal label currently used in the program
          lines,
          str,            # string, result
          formats,        # record
          i,
          translateword,  # local function
          line,
          lastresult,
          namline,
          resline,
          inline;

    # Get and check the arguments.
    format:= "ATLAS";
    avoidslots:= [];
    if   Length( arg ) = 1 then
      prog:= arg[1];
    elif 2 <= Length( arg ) and IsString( arg[2] ) then
      prog:= arg[1];
      if LowercaseString( arg[2] ) = "mtx" then
        format:= "mtx";
      fi;
    elif 2 <= Length( arg ) and IsList( arg[2] ) then
      prog:= arg[1];
      outputnames:= arg[2];
    fi;
    if Length( arg ) = 3 and IsList( arg[3] ) then
      avoidslots:= arg[3];
    fi;
    if   IsBound( prog ) and IsStraightLineProgram( prog ) then
      resused:= NrInputsOfStraightLineProgram( prog );
      lines:= LinesOfStraightLineProgram( prog );
    elif IsBound( prog ) and IsStraightLineDecision( prog ) then
      resused:= NrInputsOfStraightLineDecision( prog );
      lines:= LinesOfStraightLineDecision( prog );
    else
      Error( "usage: ",
             "AtlasStringOfProgram( <prog>[, <outputnames>] )",
             "or AtlasStringOfProgram( <prog>[, \"mtx\"] )" );
    fi;

    str:= "";
    if format = "ATLAS" then
      # Write the line of inputs.
      Append( str, "inp " );
      Append( str, String( resused ) );
      Add( str, '\n' );

      # Define the line formats.
      formats:= rec( iv:= l -> Concatenation( "iv ", String( l[1] ),
                                 " ", String( l[2] ), "\n" ),
                     cp:= l -> Concatenation( "cp ", String( l[1] ),
                                 " ", String( l[2] ), "\n" ),
                     pw:= l -> Concatenation( "pwr ", String( l[1] ),
                                 " ", String( l[2] ),
                                 " ", String( l[3] ), "\n" ),
                     mu:= l -> Concatenation( "mu ", String( l[1] ),
                                 " ", String( l[2] ),
                                 " ", String( l[3] ), "\n" ),
                     cj:= l -> Concatenation( "cj ", String( l[1] ),
                                 " ", String( l[2] ),
                                 " ", String( l[3] ), "\n" ),
                     ch:= l -> Concatenation( "chor ", String( l[1] ),
                                 " ", String( l[2] ), "\n" ),
                   );
    else
      # Write the line that describes the inputs.
      Append( str, "# inputs are expected in " );
      Append( str, JoinStringsWithSeparator( List( [ 1 .. resused ],
                                                   String ), " " ) );
      Add( str, '\n' );

      # Define the line formats.
      formats:= rec( iv:= l -> Concatenation( "ziv ", String( l[1] ),
                                 " ", String( l[2] ), "\n" ),
                     cp:= l -> Concatenation( "cp ", String( l[1] ),
                                 " ", String( l[2] ), "\n" ),
                     pw:= l -> Concatenation( "zsm pwr", String( l[1] ),
                                 " ", String( l[2] ),
                                 " ", String( l[3] ), "\n" ),
                     mu:= l -> Concatenation( "zmu ", String( l[1] ),
                                 " ", String( l[2] ),
                                 " ", String( l[3] ), "\n" ),
                     cj:= l -> Concatenation( "cj ", String( l[1] ),
                                 " ", String( l[2] ),
                                 " ", String( l[3] ), "\n" ),
                     ch:= l -> Concatenation( "chor ", String( l[1] ),
                                 " ", String( l[2] ), "\n" ),
                   );
    fi;

    # function to translate a word into a series of simple operations
    translateword:= function( word, respos )
      local used,  # maximal label, including intermediate results
            new,
            i;

      if resused < respos then
        resused:= respos;
      fi;
      used:= resused;

      if Length( word ) = 2 then

        # The word describes a simple powering.
        if word[2] = -1 then
          Append( str, formats.iv( [ word[1], respos ] ) );
        elif word[2] = 1 then
          Append( str, formats.cp( [ word[1], respos ] ) );
        elif 0 <= word[2] then
          Append( str, formats.pw( [ word[2], word[1], respos ] ) );
        else
          used:= used + 1;
          Append( str, formats.iv( [ word[1], used ] ) );
          Append( str, formats.pw( [ -word[2], used, respos ] ) );
        fi;

      elif Length( word ) = 3 and word[1] = "Order" then

        Append( str, formats.ch( [ word[2], word[3] ] ) );

      elif Length( word ) = 6 and word[2] = -1 and word[4] = 1
                              and word[6] =  1 and word[1] = word[5] then

        # The word describes a conjugation.
        Append( str, formats.cj( [ word[3], word[1], respos ] ) );

      else

        # Get rid of the powerings.
        new:= [];
        for i in [ 2, 4 .. Length( word ) ] do
          if word[i] = 1 then
            Add( new, word[ i-1 ] );
          elif 0 < word[i] then
            used:= used + 1;
            Append( str, formats.pw( [ word[i], word[ i-1 ], used ] ) );
            Add( new, used );
          else
            used:= used + 1;
            Append( str, formats.iv( [ word[ i-1 ], used ] ) );
            if word[i] < -1 then
              used:= used + 1;
              Append( str, formats.pw( [ -word[i], used-1, used ] ) );
            fi;
            Add( new, used );
          fi;
        od;

        # Now form the product of the elements in `new'.
        if Length( new ) = 1 then
          if new[1] <> respos then
            Append( str, formats.cp( [ new[1], respos ] ) );
          fi;
        elif Length( new ) = 2 then
          Append( str, formats.mu( [ new[1], new[2], respos ] ) );
        else
          used:= used + 1;
          Append( str, formats.mu( [ new[1], new[2], used ] ) );
          for i in [ 3 .. Length( new )-1 ] do
            used:= used + 1;
            Append( str, formats.mu( [ used-1, new[i], used ] ) );
          od;
          used:= used + 1;
          Append( str, formats.mu( [ used-1, new[ Length( new ) ],
                                     respos ] ) );
        fi;

      fi;
    end;

    # Loop over the lines.
    for line in lines do

      if ForAll( line, IsList ) then

        # The list describes the return values.
        lastresult:= [];
        for i in [ 1 .. Length( line ) ] do
          if Length( line[i] ) = 2 and line[i][2] = 1 then
            Add( lastresult, String( line[i][1] ) );
          else
            repeat
              resused:= resused + 1;
            until not resused in avoidslots;
            translateword( line[i], resused );
            Add( lastresult, String( resused ) );
          fi;
        od;

        if IsBound( outputnames ) then

          if Length( line ) <> Length( outputnames ) then
            Error( "<outputnames> has the wrong length" );
          fi;

          # Write the `echo' statements.
          # (Split the output specifications into lines if necessary.)
          i:= 1;
          namline:= "";
          resline:= "";
          inline:= 0;
          while i <= Length( outputnames ) do
            if    60 < Length( namline ) + Length( outputnames[i] )
               or 60 < Length( resline ) + Length( lastresult[i] ) then
              Append( str,
                  Concatenation( "echo \"Classes", namline, "\"\n" ) );
              Append( str,
                  Concatenation( "oup ", String( inline ), resline, "\n" ) );
              namline:= "";
              resline:= "";
              inline:= 0;
            fi;
            Add( namline, ' ' );
            Add( resline, ' ' );
            Append( namline, outputnames[i] );
            Append( resline, lastresult[i] );
            inline:= inline + 1;
            i:= i + 1;
          od;
          if inline <> 0 then
            Append( str,
                Concatenation( "echo \"Classes", namline, "\"\n" ) );
            Append( str,
                Concatenation( "oup ", String( inline ), resline, "\n" ) );
          fi;

        elif ForAll( [ 1 .. Length( line ) ], i -> line[i] = [ i, 1 ] ) then

          # Write a short output statement.
          if format = "ATLAS" then
            Append( str, "oup " );
            Append( str, String( Length( line ) ) );
            Add( str, '\n' );
          else
            Append( str, "echo \"outputs are in " );
            Append( str, JoinStringsWithSeparator(
                           List( [ 1 .. Length( line ) ], String ), " " ) );
            Append( str, "\"\n" );
          fi;

        elif format = "ATLAS" then

          # Write the full output statements.
          i:= 1;
          resline:= "";
          inline:= 0;
          while i <= Length( lastresult ) do
            if 60 < Length( resline ) + Length( lastresult[i] ) then
              Append( str,
                  Concatenation( "oup ", String( inline ), resline, "\n" ) );
              resline:= "";
              inline:= 0;
            fi;
            Add( resline, ' ' );
            Append( resline, lastresult[i] );
            inline:= inline + 1;
            i:= i + 1;
          od;
          if inline <> 0 then
            Append( str,
                Concatenation( "oup ", String( inline ), resline, "\n" ) );
          fi;

        else

          Append( str, "echo \"outputs are in " );
          Append( str, JoinStringsWithSeparator( lastresult, " " ) );
          Append( str, "\"\n" );

        fi;

        # Return the result.
        return str;

      else

        # Separate word and position where to put the result,
        # and translate the line into a sequence of simple steps.
        if ForAll( line, IsInt ) then
          resused:= resused + 1;
          lastresult:= resused;
          translateword( line, resused );
        elif line[1] = "Order" then
          translateword( line, "dummy" );
        else
          lastresult:= line[2];
          translateword( line[1], lastresult );
        fi;

      fi;
    od;

    # (If we arrive here then there is exactly one output value.)

    # Write the `echo' statements if applicable.
    # (This isn't really probable, is it?)
    if IsBound( outputnames ) then
      if Length( outputnames ) <> 1 then
        Error( "<outputnames> has the wrong length" );
      fi;
      Append( str,
          Concatenation( "echo \"Classes ", outputnames[1], "\"\n" ) );
    fi;

    # Write the output statement in the case of straight line programs.
    if IsStraightLineProgram( prog ) then
      if format = "ATLAS" then
        Append( str, "oup 1 " );
        Append( str, String( lastresult ) );
        Add( str, '\n' );
      else
        Append( str, "echo \"outputs are in " );
        Append( str, String( lastresult ) );
        Append( str, "\"\n" );
      fi;
    fi;

    # Return the result;
    return str;
end );


#############################################################################
##
#E

