#############################################################################
####
##
#W  options.gi                 ACE Package                        Greg Gamble
##
##  This file installs functions and records for manipulating ACE options.
##
#Y  Copyright (C) 2000  Centre for Discrete Mathematics and Computing
#Y                      Department of Information Technology & Electrical Eng.
#Y                      University of Queensland, Australia.
##

#############################################################################
####
##
#V  KnownACEOptions . . . . . . record whose fields are the known ACE options
##  . . . . . . . . . . . . . . . . . .  each field is assigned a list of two
##  . . . . . . . . . . . . . . . . . .  components:  [leastlength, listorfn]
##
##  The known ACE options  are the RecNames of KnownACEOptions.  The value of
##  of each RecName is a list [ leastlength, listorfn ], where leastlength is
##  an integer specifying the least length of an abbreviation of the  RecName
##  that will match an ACE option,  and listorfn is either a list of  allowed
##  values or a function that can be used to test that the value of an option
##  is valid e.g. for the RecName "lookahead", we have knownOptions.lookahead
##  equal to [ 4, [0..4] ] which indicates that "look", "looka", etc. are all
##  valid abbreviations of the "lookahead" option,  and the values  that that
##  option can take are in the (integer) range 0 to 4.
##
##  If the allowed values listed for an option are 0 and 1,  then  false  and
##  true are also permitted (we translate false and true to 0 and 1, respect-
##  ively when we call ACE). The empty string signifies that ACE  expects  no
##  value for that option.
##
##  Only single-word versions of options can be used by a user of ACE via the
##  GAP interface e.g. "cc" is a synonym for "coset coincidence"  as  an  ACE
##  option,  but the latter,  being 2 words,  is  not available via  the  GAP
##  interface.
##
##  The commented out options are known ACE options that probably  won't work
##  via the GAP interface ... if the user uses these  the  interface  program
##  CALL_ACE will complain: `unknown (possibly new) or bad'  but  still  pass
##  these options to ACE ... at least the user will then know if ACE does not
##  respond as expected that the options should not be used.  We usually only
##  warn that certain options might be bad, so that this interface has a good
##  chance of still being functional if new options are added to the ACE bin-
##  ary.
##
##  Some  options  are  `GAP-introduced'  i.e. technically they are  not  ACE
##  options  ...  there is a comment beside such options;  and  they are also
##  listed in NonACEbinOptions below.
##

InstallValue(KnownACEOptions, rec(
  # aceinfile, aceignore, aceignoreunknown, acenowarnings, silent (and
  # further down: aceoutfile) are GAP-introduced options ... they  are
  # not ACE binary options.
  aceinfile := [5, IsString],
  aceignore := [5, x -> IsList(x) and ForAll(x, xi -> IsString(xi))],
  aceignoreunknown := [10, x -> IsList(x) and ForAll(x, xi -> IsString(xi))],
  acenowarnings := [6, [0,1]],
  aceecho := [7, [""]],
  aceincomment := [6, IsString],
  aceexampleoptions := [17, [0,1]],
  silent := [6, [0,1]],
  lenlex := [6, [0,1]],
  semilenlex := [10, [0,1]],
  incomplete := [10, [0,1]],
  sg := [2, IS_ACE_STRINGS],
  rl := [2, IS_ACE_STRINGS],
  aep  := [3, [1..7]],
  ai := [2, IsString],
  ao   := [2, IsString],      # "aceoutfile" is a GAP-introduced
  aceoutfile := [4, IsString],# synonym for "ao"
  asis := [2, [0,1]],
  begin := [3, [""]],         # "begin" and "start" are synomyms
  start := [5, [""]],         # ... "end" synonym omitted (it is a GAP keyword)
  bye := [3, [""]],           # "bye", "exit" and "qui" are synonyms
  exit := [4, [""]],
  qui := [1, [""]],           # the "quit" form is not available since
                              # it's a GAP keyword
  cc   := [2, x -> IsInt(x) and x > 1],
  cfactor := [1, IsInt],      # "cfactor" and "ct" are synonyms
  ct   := [2, IsInt],
  check := [5, [""]],
  redo := [4, [""]],
  compaction := [3, [0..100]],
  continu := [4, [""]],       # "continue" is a GAP 4.3+ keyword
  cycles := [2, [""]],
  dmode := [4, [0..4]],
  dsize := [4, x -> x = 0 or IsPosInt(x)],
  default := [3, [""]],
  ds := [2, IS_INC_POS_INT_LIST],
  dr := [2, IS_INC_POS_INT_LIST],
  dump := [1, x -> x in ["",0,1,2] or
                   (IsList(x) and x[1] in [0..2] and
                    (Length(x) = 1 or (Length(x) = 2 and x[2] in [0,1])))],
  easy := [4, [""]],
  echo := [4, [0,1,2]],       # hijacked! ... we don't pass this to ACE
  enumeration := [4, IsString],
  felsch := [3, ["",0,1]],
  ffactor := [1, x -> x = 0 or IsPosInt(x)],# "ffactor" and "fill"
  fill := [3, x -> x = 0 or IsPosInt(x)],   # are synonyms ... there is
                                            # no "fi" since it's a GAP
                                            # keyword
  ## Most interface functions require the next 3 ACE options to be
  ## passed as arguments rather than options
  group := [2, x -> IsInt(x) or IsString(x) or
                    (IsList(x) and
                     ForAll(x, xi -> IsString(xi) and
                                     (Length(xi) = 1) and
                                     IsLowerAlphaChar( xi[1] )))],
                                               # For group generators
  generators := [3, IS_ACE_STRINGS],           # For subgroup generators
  relators := [3, IS_ACE_STRINGS],             # For group relators

  hard := [2, [""]],
  help := [1, [""]],
  hlt  := [3, [""]],
  hole := [2, [-1..100]],
  lookahead := [4, [0..4]],
  loop := [4, x -> x = 0 or IsPosInt(x)],
  max  := [3, x -> x = 0 or (IsInt(x) and x >= 2)],
  mendelsohn := [4, [0,1]],
  messages := [4, IsInt],   # "messages" and "monitor" are synonyms
  monitor := [3, IsInt],
  mode := [2, [""]],
  nc   := [2, ["",0,1]],    # "nc" and "normal" are synonyms
  normal := [6, ["",0,1]],
  no   := [2, x -> IsInt(x) and x >= -1],
  options := [3, [""]],
  oo   := [2, IsInt],       # "oo" and "order" are synonyms
  order := [5, IsInt],
  #parameters := [3, [""]], # decommissioned ACE option
  path := [4, [0,1]],
  pmode := [4, [0..3]],
  psize := [4, x -> x = 0 or
                    (IsInt(x) and IsEvenInt(x) and IsPrimePowerInt(x))],
  sr := [2, ["",0,1,2,3,4,5]],
  print := [2, x -> x = "" or IsInt(x) or
                    (IsList(x) and Length(x) <= 3 and IsInt(x[1]) and
                     ForAll(x{[2..Length(x)]}, IsPosInt)) ],
  purec := [5, [""]],       # the ACE option is "pure c"
  purer := [5, [""]],       # the ACE option is "pure r"
  rc   := [2, x -> x = "" or IsInt(x) or
                   (IsList(x) and Length(x) <= 2 and ForAll(x, IsInt))],
  recover := [4, [""]],     # "recover" and "contiguous"
  contiguous := [6, [""]],  # are synonyms ... "rec" is
                            # not an allowed abbreviation
                            # since it's a GAP  keyword
  rep  := [2, x -> x in [1..7] or
                   (IsList(x) and Length(x) <= 2 and x[1] in [1..7] and
                    ForAll(x{[2..Length(x)]}, IsInt))],
  #restart := [7, [""]],    # decommissioned ACE option
  rfactor := [1, IsInt],    # "rfactor" and "rt" are synonyms
  rt   := [2, IsInt],
  row  := [3, [0,1]],
  sc   := [2, IsInt],       # "sc" and "stabilising" are synonyms
  stabilising := [6, IsInt],
  sims := [4, [1,3,5,7,9]],
  standard := [2, [""]],
  statistics := [4, [""]],  # "statistics" and "stats" are synonyms
  stats := [5, [""]],
  style := [5, [""]],
  subgroup := [4, IsString],
  system := [3, IsString],
  text := [4, IsString],
  time := [2, x -> IsInt(x) and x >= -1],
  tw   := [2, x -> IsList(x) and Length(x) = 2 and
                   IsInt(x[1]) and IsWord(x[2])],
  trace := [2, x -> IsList(x) and Length(x) = 2 and
                    IsInt(x[1]) and IsWord(x[2])],
  workspace := [2, x -> IsInt(x) or
                        (IsString(x) and x[Length(x)] in "0123456789kmgKMG")]
));

#############################################################################
####
##
#V  ACEOptionSynonyms . . . . . record whose fields are `preferred' known ACE
##  . . . . . . . . . . . . . . options that have synonyms.  The  values  are
##  . . . . . . . . . . . . . . . . . . . . lists of synonymous alternatives.
##
##

InstallValue(ACEOptionSynonyms, rec(
  ao   := ["aceoutfile"],
  ct   := ["cfactor"],
  fill := ["ffactor"],
  messages := ["monitor"],
  nc   := ["normal"],
  order := ["oo"],
  recover := ["contiguous"],
  rt   := ["rfactor"],
  sc   := ["stabilising"],
  tw   := ["trace"],
  stats := ["statistics"],
  start := ["begin"],
  bye  := ["exit", "qui"],
  redo := ["check"]
));

#############################################################################
####
##
#V  NonACEbinOptions . . . . . . . list of known ACE options that are not ACE
##  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . binary options.
##

InstallValue(NonACEbinOptions,
  [ "aceinfile",     "aceoutfile", "aceignore",    "aceignoreunknown",
    "acenowarnings", "aceecho",    "aceincomment", "aceexampleoptions",
    "echo",          "silent",     "lenlex",       "semilenlex",
    "incomplete" ]
);

#############################################################################
####
##
#V  ACE_INTERACT_FUNC_OPTIONS . . . . . list of non ACE options that are used
##  . . . . . . . . . . . . . . . . . . . .  by the interaction ACE functions
##

InstallValue(ACE_INTERACT_FUNC_OPTIONS,
  [ # used by: ACEConjugatesForNormalClosure
    "add",
    # used by: ACEOrders
    "suborder",
    # used by: ACERandomlyApplyCosetCoincidence
    "attempts", "hibound", "lobound", "subindex" ]
);

#############################################################################
####
##
#V  ACEParameterOptions . .  record whose fields are the known ACE  parameter
##  . . . . . . . . . . . .  options.  Each  field is  assigned   the   known
##  . . . . . . . . . . . .  default value, or is a record of default values.
##
##  An ACE `parameter' option, is a known ACE option for which the ACE binary
##  has a default value.  These are the `Run Parameters' that ACE lists  with
##  the `sr: 1' command,  except for  `group',  `relators'  and  `generators'
##  (which the user provides a value for via arguments rather than options).
##
##  For the case that the value of a field of the ACEParameterOptions  record
##  is itself  a  record,  the  fields  of  that  record  are  `default'  and
##  strategies for which the value assigned by that strategy differs from the
##  `default' strategy. A strategy here means a strategy option  concatenated
##  with any of its possible values (as strings).
##

InstallValue(ACEParameterOptions, rec(
  asis := 0,
  # `ct' is synonymous with `cfactor' but here we list just once.
  ct   := rec(default := 0, felsch0 := 1000, felsch1 := 1000,
              hard := 1000, purec := 1000,   sims9 := 1000),
  compaction := rec(default := 10, easy := 100, purec := 100, purer := 100),
  dmode := rec(default := 4, easy := 0,  hlt := 0,
               purer := 0,   sims1 := 0, sims5 := 0),
  dsize := rec(default := 1000),
  enumeration := "G",
  # `fill' is synonymous with `ffactor' but here we list just once.
  fill := rec(default := 0, easy := 1,  felsch0 := 1, hlt := 1,
              purec := 1,   purer := 1, sims1 := 1,   sims3 := 1,
              sims5 := 1,   sims7 := 1, sims9 := 1),
  hole := -1,
  lookahead := rec(default := 0, hlt := 1),
  loop := 0,
  max  := 0,
  mendelsohn := rec(default := 0, sims5 := 1, sims7 := 1),
  messages := 0, # Synonymous with `monitor' but here we list just once.
  no   := rec(default := -1, easy := 0,  felsch0 := 0, hlt := 0,
              purec := 0,    purer := 0, sims1 := 0,   sims3 := 0,
              sims5 := 0,    sims7 := 0, sims9 := 0),
  path := rec(default := 0),
  pmode := rec(default := 3, easy := 0,  felsch0 := 0, hlt := 0,
               purec := 0,   purer := 0, sims1 := 0,   sims3 := 0,
               sims5 := 0,   sims7 := 0, sims9 := 0),
  psize := rec(default := 256),
  # `rt' is synonymous with `rfactor' but here we list just once.
  rt   := rec(default := 0,   easy := 1000,  hard := 1,
              hlt := 1000,    purer := 1000, sims1 := 1000,
              sims3 := -1000, sims5 := 1000, sims7 := -1000),
  row  := rec(default := 1, felsch0 := 0, felsch1 := 0,
              purec := 0,   purer := 0,   sims9 := 0),
  subgroup := "H",
  time := -1,
  workspace := 1000000
));

#############################################################################
####
##
#V  ACEStrategyOptions  . list of known ACE options that are strategy options
##

InstallValue(ACEStrategyOptions,
  [ "default", "easy", "felsch", "hard", "hlt", "purec", "purer", "sims" ]
);

#############################################################################
####
##
#V  ACE_OPT_TRANSLATIONS  . . . . . record of ACE interface options for which
##  . . . . . . . . . . . . . . . . . the  ACE  binary has a different  name;
##  . . . . . . . . . . . . . . . . . its fields are the ACE interface names,
##  . . . . . . . . . . . . . . . . . its values are the  ACE  binary  names.
##

InstallValue(ACE_OPT_TRANSLATIONS, rec(
  purec := "pure c", # These first two haven't been called NonACEbinOptions
  purer := "pure r",
  aceoutfile := "ao",
  aceecho := "echo",
  aceincomment := "#"
));

#############################################################################
####
##
#V  ACE_OPT_ACTIONS . . . . . . . record of special actions  of  ACE  options
##  . . . . . . . . . . . . . . . its fields are the ACE  option  names  with
##  . . . . . . . . . . . . . . . special actions, its values are the actions
##

InstallValue(ACE_OPT_ACTIONS, rec(
  purec := "passed to ACE via option: pure c",
  purer := "passed to ACE via option: pure r",
  aceoutfile := "passed to ACE via option: ao",
  aceecho := "passed to ACE via option: echo",
  aceincomment := "passed as an ACE comment, behind a '#'",
  aceexampleoptions := "inserted by ACEExample, not passed to ACE"
));

#############################################################################
####
##
#V  ACE_ERRORS . . . . . . . . . . . . record of ACE interface error messages
##
##

InstallValue(ACE_ERRORS, rec(
  argnotopt := "should be passed as an argument, NOT an option"
));

#############################################################################
####
##
#V  ACE_OPT_SENTINELS . . . . . . . . . . . . . .  record of option sentinels
##
##  is a record whose fields are the  preferred  option  name  of  those  ACE
##  options that normally produce output and whose values are  either  `fail'
##  if there is no reliable way of detecting the last line  of  output  or  a
##  function of an input line <line> that returns `true'  if  <line>  is  the
##  last line of output expected for an option.
##

InstallValue(ACE_OPT_SENTINELS, rec(
  start := line -> Length(line) > 1 and line[ Length(line) - 1 ] = ')',
  redo  := line -> Length(line) > 1 and line[ Length(line) - 1 ] = ')',
  continu := line -> Length(line) > 1 and line[ Length(line) - 1 ] = ')',
  aep  := line -> IsMatchingSublist(line, "* P"),
  rep  := fail,
  cc   := line -> IsMatchingSublist(line, "Coset"),
  mode := line -> IsMatchingSublist(line, "start ="),
  nc   := fail,
  order := fail,
  options := line -> IsMatchingSublist(line, "  host info"),
  dump  := line -> IsMatchingSublist(line, "  #----"),
  sr    := line -> IsMatchingSublist(line, "  #----"),
  stats := line -> IsMatchingSublist(line, "  #----"),
  print := fail,
  rc   := line -> Length(line) > 12 and
                  line{[1..13]} in ["* No success;", "* An appropri",
                                    "   finite ind", "   * Unable t"],
  cycles := line -> Length(line) > 1 and line{[1..2]} in ["CO", "co"],
  recover := line -> Length(line) > 1 and line{[1..2]} in ["CO", "co"],
  standard := line -> Length(line) > 1 and line{[1..2]} in ["CO", "co"],
  sc   := fail,
  style := line -> IsMatchingSublist(line, "style ="),
  test := fail,
  tw   := line -> PositionSublist(line, "* word =") <> fail or
                  IsMatchingSublist(line, "* Trace ")
));

#############################################################################
####
##
#F  IS_INC_POS_INT_LIST . . . . . . Internal function used in KnownACEOptions
##  . . . . . . . .  returns true if argument is a single positive integer or
##  . . . . . . . . . . .  is a strictly increasing list of positive integers
##
InstallGlobalFunction(IS_INC_POS_INT_LIST,
  x -> IsPosInt(x) or (IsPosInt(x[1]) and IsSSortedList(x)));

#############################################################################
####
##
#F  IS_ACE_STRINGS  . . . . . . . . Internal function used in KnownACEOptions
##  . . . . . . . . . returns true if argument is a string or list of strings
##
InstallGlobalFunction(IS_ACE_STRINGS,
  x -> IsString(x) or (IsList(x) and ForAll(x, xi -> IsString(xi))));

#############################################################################
####
##
#F  IsKnownACEOption  . . . . . . . . Returns true if optname is a mixed case
##  . . . . . . . . . . . . . . . . . abbreviation    of    a    field     of
##  . . . . . . . . . . . . . . . . . .  KnownACEOptions, or false otherwise.
##
InstallGlobalFunction(IsKnownACEOption,
  optname -> ACEOptionData(optname).known);

#############################################################################
####
##
#F  ACEPreferredOptionName  . . . . Returns the lowercase unabbreviated first
##  . . . . . . . . . . . . . . . . alternative of optname if it is  a  known
##  . . . . . . . . . . . . . . . . ACE  option,  or  optname  in  lowercase,
##  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  otherwise.
##
InstallGlobalFunction(ACEPreferredOptionName,
  optname -> ACEOptionData(optname).synonyms[1]);

#############################################################################
####
##
#F  IsACEParameterOption  . . Returns true if ACEPreferredOptionName(optname)
##  . . . . . . . . . . . . . . . . . . . . is a field of ACEParameterOptions
##
InstallGlobalFunction(IsACEParameterOption,
  optname -> ACEPreferredOptionName(optname) in RecNames(ACEParameterOptions));

#############################################################################
####
##
#F  IsACEStrategyOption . . . Returns true if ACEPreferredOptionName(optname)
##  . . . . . . . . . . . . . . . . . . . . . . . .  is in ACEStrategyOptions
##
InstallGlobalFunction(IsACEStrategyOption,
  optname -> ACEPreferredOptionName(optname) in ACEStrategyOptions);

#############################################################################
####
##
#F  ACE_OPTIONS . . . . . . . . . . . . . . . . . . . . . . Internal function
##  . . . . . . . . . returns the options passed to an ACE interface function
##
##
InstallGlobalFunction(ACE_OPTIONS, function()
  if IsEmpty(OptionsStack) then
    return rec();
  else
    return OptionsStack[ Length(OptionsStack) ];
  fi;
end);

#############################################################################
####
##
#F  ACE_OPT_NAMES . . . . . . . . . . . . . . . . . . . . . Internal function
##  . . . . . . . .  returns option names passed to an ACE interface function
##  . . . . . . . . . . . . . if acenowarnings is not an option it also warns
##  . . . . . . . . . . . . . . . . . . . . . . . .  about deprecated options
##
InstallGlobalFunction(ACE_OPT_NAMES, function()
local optnames;
  optnames := RecNames(ACE_OPTIONS());
  if not VALUE_ACE_OPTION(optnames, false, "acenowarnings") then
    if "messfile" in optnames then
      Info(InfoACE + InfoWarning, 1,
           "ACE Warning: ",
           "Option `messfile' deprecated: use `ACEoutfile' instead");
    elif "outfile" in optnames then
      Info(InfoACE + InfoWarning, 1,
           "ACE Warning: ",
           "Option `outfile' deprecated: use `ACEinfile' instead");
    fi;
  fi;
  return optnames;
end);

#############################################################################
####
##
#F  MATCHES_KNOWN_ACE_OPT_NAME  . . . . . . . . . . . . . . Internal function
##  . . . .  returns true iff optname is a valid abbreviation of knownoptname
##  . . . . . . . . . . . . . . . . . optname should be in lowercase already!
##
InstallGlobalFunction(MATCHES_KNOWN_ACE_OPT_NAME,
function(knownoptname, optname)
  return IsMatchingSublist(knownoptname, optname) and
         KnownACEOptions.(knownoptname)[1] <= Length(optname);
end);

#############################################################################
####
##
#F  FULL_ACE_OPT_NAME . . . . . . . . . . . . . . . . . .  Internal procedure
##  . . . . . . sets opt.fullname to be the unabbreviated version of opt.name
##  . . . . . . . . . . .  if one exists among the fields of KnownACEOptions,
##  . . . . . . . . . . . . . . in which case, opt.known is also set to true;
##  . . . . . . . . . . .  otherwise,  opt.fullname  is set  to  opt.name  in
##  . . . . . . . . . . . . . . lower case,  and  opt.known  is set to false.
##
InstallGlobalFunction(FULL_ACE_OPT_NAME, function(opt)
local lcaseoptname, list;
  lcaseoptname := LowercaseString(opt.name);
  list := Filtered(RecNames(KnownACEOptions),
                   s -> MATCHES_KNOWN_ACE_OPT_NAME(s, lcaseoptname));
  opt.known := not( IsEmpty(list) );
  if opt.known then
    opt.fullname := list[1];  # We assume any match is unique!
  else
    opt.fullname := lcaseoptname;
  fi;
end);

#############################################################################
####
##
#F  ACE_OPTION_SYNONYMS . . . . . . . . . . . . . . . . . . Internal function
##  . . . . . . . . . . . . . . . . . . returns a list of synonyms of optname
##
##
InstallGlobalFunction(ACE_OPTION_SYNONYMS, function(optname)
local list, recname;
  list := [ optname ];
  for recname in RecNames(ACEOptionSynonyms) do
    if recname = optname or optname in ACEOptionSynonyms.(recname) then
      list := Concatenation( [ recname ], ACEOptionSynonyms.(recname) );
      break;
    fi;
  od;
  return list;
end);

#############################################################################
####
##
#F  ACE_IF_EXPR . . . . . . . . . . . . . . . . . . . . . .  An expression if
##
##
InstallGlobalFunction(ACE_IF_EXPR, function(bool, trueval, falseval, failval)
  if bool = true then
    return trueval;
  elif bool = false then
    return falseval;
  else
    return failval;
  fi;
end);

#############################################################################
####
##
#F  ACE_VALUE_OPTION  . . . . . . . . Essentially an extension of ValueOption
##  . . . . . . . . . . . . . . .  but also removes optname from OptionsStack
##
##  ACE_VALUE_OPTION(optname,  defaultval)  returns  ValueOption(optname)  if
##  optname is set and defaultval, otherwise.
##
##  ACE_VALUE_OPTION(optname, val,  trueval,  elseval).  If optname has value
##  val then return trueval else return elseval.
##
##  If ACE_VALUE_OPTION is called with a different no. of aguments to 1 or 2,
##  all but  the  first  argument  is  ignored,  and  ValueOption(arg[1])  is
##  returned. Calling ACE_VALUE_OPTION with no arguments is an error.
##
InstallGlobalFunction(ACE_VALUE_OPTION, function(arg)
local optval;
  optval := ValueOption(arg[1]);
  if not IsEmpty(OptionsStack) then
    Unbind( OptionsStack[ Length(OptionsStack) ].(arg[1]) );
  fi;
  if Length(arg) = 2 then
    return ACE_IF_EXPR(optval <> fail, optval, arg[2], arg[2]);
  elif Length(arg) = 4 then
    return ACE_IF_EXPR(optval = arg[2], arg[3], arg[4], arg[4]);
  elif not IsEmpty(arg) then
    # Ignore all but the first argument
    return optval;
  fi;
end);

#############################################################################
####
##
#F  ACE_VALUE_OPTION_ERROR(<optrec>, <optname>, <defaultval>, <IsOK>, <errmsg>)
##
##  returns:
##    `false' if `ValueOption(<option>) = fail'
##               (and sets `<optrec>.(<optname>) := <defaultval>') or
##            if `<IsOK>( ValueOption(<option>) )'
##               (and sets `<optrec>.(<optname>) := ValueOption(<option>)')
##    `true'  if `not <IsOK>( ValueOption(<option>) )'
##               (and sets `<optrec>.errmsg := [<errmsg>]')
##
InstallGlobalFunction(ACE_VALUE_OPTION_ERROR,
function(optrec, optname, defaultval, IsOK, errmsg)
local optval;
  optval := ValueOption(optname);
  if optval = fail then
    optrec.(optname) := defaultval;
  elif IsOK(optval) then
    optrec.(optname) := optval;
  else
    optrec.errmsg := [errmsg];
    return true;
  fi;
  return false;
end);

#############################################################################
####
##
#F  VALUE_ACE_OPTION  . . . . . . . . . . . . . . . . . . . Internal function
##  . . . . . . . checks among optnames for any settings of synonyms of optnm
##  . . . . . . . (or if optnm is a list  any  synonyms  of  the  members  of
##  . . . . . . . optnm). The latest such optname in  optnames  will  prevail
##  . . . . . . . and its value will be returned. Otherwise, if  there  isn't
##  . . . . . . . . . . . . . . . .  such an optname, defaultval is returned.
##
InstallGlobalFunction(VALUE_ACE_OPTION, function(optnames, defaultval, optnm)
local optname, optval, optnmlist;
  optval := defaultval;
  if IsString(optnm) then
    optnmlist := [ optnm ];
  else
    optnmlist := optnm; # This situation is special ... useful for checking
                        # whether a list of options have been set
  fi;
  optnmlist := Union( List(optnmlist,
                           optname -> ACE_OPTION_SYNONYMS(optname)) );
  for optname in Filtered(optnames,
                          optname -> ForAny(optnmlist,
                                            s ->
                                            MATCHES_KNOWN_ACE_OPT_NAME(
                                                s,
                                                LowercaseString(optname)
                                                )
                                            ))
  do
    optval := ValueOption(optname);
  od;
  return optval;
end);

#############################################################################
####
##
#F  DATAREC_VALUE_ACE_OPTION  . . . . . . . . . . . . . . . Internal function
##  . . . . . . . checks among RecNames(datarec.options) for any settings  of
##  . . . . . . . synonyms of optnm The latest such optname prevails and  its
##  . . . . . . . value is  returned.  Otherwise,  if  there  isn't  such  an
##  . . . . . . . optname  or  datarec.options  is  unbound,  defaultval   is
##  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . returned.
##
InstallGlobalFunction(DATAREC_VALUE_ACE_OPTION,
                      function(datarec, defaultval, optnm)
local optname, optval;
  optval := defaultval;
  if IsBound(datarec.options) then
    for optname in Filtered(RecNames(datarec.options),
                            optname -> ForAny(ACE_OPTION_SYNONYMS(optnm),
                                              s ->
                                              MATCHES_KNOWN_ACE_OPT_NAME(
                                                  s,
                                                  LowercaseString(optname)
                                                  )
                                              ))
    do
      optval := datarec.options.(optname);
    od;
  fi;
  return optval;
end);

#############################################################################
####
##
#F  ACE_COSET_TABLE_STANDARD  . . . . . . Return either the user's choice for
##  . . . . . . . . . . . . . . . . . . . the CosetTableStandard or,  if  the
##  . . . . . . . . . . . . . . . . . . . user has made no choice,  a  string
##  . . . . . . . . . . . . . . . . . . . representing   the   current    GAP
##  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . standard.
##
##  A check among options for any settings of `lenlex' or  `semilenlex'.  The
##  latest such optname that is set to true is returned, or if  there  is  no
##  such setting a string representing the current GAP default  is  returned:
##  for  GAP  4.2  "GAPsemilenlex"  was  returned;  since  GAP   4.3,   "GAP"
##  concatenated  with  the  value  of  `CosetTableStandard'   (by   default,
##  "lenlex") is returned.
##
InstallGlobalFunction(ACE_COSET_TABLE_STANDARD, function(options)
local optname;
  for optname in Filtered(Reversed( RecNames(options) ),
                          optname -> ForAny(["lenlex", "semilenlex"],
                                            s ->
                                            MATCHES_KNOWN_ACE_OPT_NAME(
                                                s,
                                                LowercaseString(optname)
                                                )
                                            ))
  do
    if options.(optname) = true then
      return ACEPreferredOptionName(optname);
    fi;
  od;
  return Concatenation("GAP", CosetTableStandard);
end);

#############################################################################
####
##
#F  ACE_VALUE_ECHO  . . . . . . . . . . . . . . . . . . . . Internal function
##
##
InstallGlobalFunction(ACE_VALUE_ECHO, function(optnames)
local echoval;
  echoval := VALUE_ACE_OPTION(optnames, 0, "echo");
  if echoval in KnownACEOptions.echo[2] then
    return echoval;
  else
    return ACE_IF_EXPR(echoval = true, 1, 0, 0);
  fi;
end);

#############################################################################
####
##
#F  TO_ACE_GENS . . . . . . . . . . . . . . . . . . . . . . Internal function
##  . . . . . . . . . . . . from the GAP free group generators fgens  returns
##  . . . . . . . . . . . . a record used to create the equivalent ACE  group
##  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  generators
##
##  Returns a record with fields:
##
##    acegens
##        the ACE equivalent of fgens; and
##
##    toace
##        the ACE directive string needed for the `group' option so that  ACE
##        uses acegens for its generators.
##
InstallGlobalFunction(TO_ACE_GENS, function(fgens)
local n, acegens;

  n := Length(fgens);
  # Define the generators ACE will use
  if n <= 26 then
    # if #generators <= 26 tell ACE to use alphabetic generators: a ...
    if ForAll(fgens, function(g)
                       local gstring;
                       gstring := String(g);
                       return Length(gstring) = 1 and
                              LowercaseString(gstring) = gstring;
                     end)
    then
      # if all generators are represented by single lowercase letters
      # ... use the user's set of generators for ACE
      acegens := List(fgens, g -> String(g));
    else
      acegens := List([1..n], i -> WordAlp(CHARS_LALPHA, i));
    fi;
    return rec(acegens := acegens, toace := Flat(acegens));
  else
    # if #generators > 26 tell ACE to use numerical generators: 1 ...
    return rec(acegens := List([1..n], i -> String(i)), toace := n);
  fi;
end);

#############################################################################
####
##
#F  ACE_WORDS . . . . . . . . . . . . . . . . . . . . . . . Internal function
##  . . . . . . . . . .  returns the translation of words in generators fgens
##  . . . . . . . . . . .  to words in ACEgens (the generators ACE will use),
##  . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  as one string.
##
InstallGlobalFunction(ACE_WORDS, function(words, fgens, ACEgens)
  words := ACE_WORDS_ARG_CHK(fgens, words, "");
  return JoinStringsWithSeparator(
             List(words, w -> String( MappedWord( w,
                                                  fgens,
                                                  GeneratorsOfGroup(
                                                      FreeGroup(ACEgens)
                                                      ) ) ) ) );
end);

#############################################################################
####
##
#F  ACE_RELS  . . . . . . . . . . . . . . . . . . . . . . . Internal function
##  . . . . . . . . . . . returns the translation of  the  relators  rels  in
##  . . . . . . . . . . . generators  fgens  to   words   in   ACEgens   (the
##  . . . . . . . . . . . generators ACE will use), as  one  string,  but  if
##  . . . . . . . . . . . enforceAsis is true  ensure  the  relator  for  the
##  . . . . . . . . . . . first generator (which we'll  represent  as  x)  is
##  . . . . . . . . . . . . . . . . .  translated as "x*x" rather than "x^2".
##
InstallGlobalFunction(ACE_RELS, function(rels, fgens, ACEgens, enforceAsis)
  if enforceAsis then
    return Concatenation( ACEgens[1], ACEgens[1], ", ",
                          ACE_WORDS(Filtered(rels, rel -> rel <> fgens[1]^2),
                                    fgens, ACEgens) );
  else
    return ACE_WORDS(rels, fgens, ACEgens);
  fi;
end);

#############################################################################
####
##
#F  ToACEGroupGenerators  . . . . . Given the GAP free group generators fgens
##  . . . . . . . . . . . . . . . . returns the ACE directive  string  needed
##  . . . . . . . . . . . . . . . . for the `group' option so that  ACE  uses
##  . . . . . . . . . . . . . . . . an   appropriate   equivalent   set    of
##  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . generators.
##
InstallGlobalFunction(ToACEGroupGenerators, function(fgens)

  fgens := ACE_FGENS_ARG_CHK(fgens);
  return TO_ACE_GENS(fgens).toace;
end);

#############################################################################
####
##
#F  ToACEWords  . . . .  Returns the translation of words in generators fgens
##  . . . . . . . . . .  to equivalent ACE words, as one string, suitable for
##  . . . . . . . . . . . . . . . .  the `relators' and `generators' options.
##
InstallGlobalFunction(ToACEWords, function(fgens, words)

  fgens := ACE_FGENS_ARG_CHK(fgens);
  return ACE_WORDS(words, fgens, TO_ACE_GENS(fgens).acegens);
end);

#############################################################################
####
##
#F  ACE_FGENS_ARG_CHK( <fgens> )
##
##  Checks that <fgens> is a list of free group generators for the same  free
##  group, gives the user a chance to fix them if necessary, and then returns
##  the (repaired) <fgens>.
##
InstallGlobalFunction(ACE_FGENS_ARG_CHK, function(fgens)
local errmsg, onbreakmsg, error, fam;

  onbreakmsg :=
      ["Type: 'quit;' to quit to outer loop, or",
       "type: 'fgens := <val>; return;' to assign <val> to fgens to continue."];
  error := true;
  repeat
    if not IsList(fgens) then
        errmsg := ["fgens must be a *list* of free group gen'rs"];
    elif not ForAll(fgens, g -> IsAssocWordWithInverse(g) and
                                (NumberSyllables(g) = 1) and
                                (ExponentSyllable(g, 1) = 1)) then
      if ForAll(fgens, IsElementOfFpGroup) then
        errmsg := ["fgens must be a list of free group gen'rs,",
                   "not fp group elements e.g. use 'FreeGeneratorsOfFpGroup'",
                   "rather than 'GeneratorsOfGroup'"];
      else
        errmsg := ["fgens must be a list of free group gen'rs"];
      fi;
    else
      fam := FamilyObj(fgens[1]);
      if not ForAll(fgens{[2..Length(fgens)]}, g -> fam = FamilyObj(g)) then
        errmsg := ["fgens must all belong to the same free group"];
      else
        error := false;
      fi;
    fi;
    if error then
      Error(ACE_ERROR(errmsg, onbreakmsg), "\n");
    fi;
  until not error;
  return fgens;
end);

#############################################################################
####
##
#F  ACE_WORDS_ARG_CHK( <fgens>, <words>, <whicharg> )
##
##  Checks that <words> is a valid list of words in the free group generators
##  <fgens>. If not, an error message for  the  <whicharg>  (which  indicates
##  what type of words they are,  e.g.  "relators",  "subgp  gen'rs"  or  "")
##  argument is generated, telling the user how  to  fix  the  problem.  Once
##  everything is ok, <words> after being filtered of any  identity  elements
##  is returned.
##
InstallGlobalFunction(ACE_WORDS_ARG_CHK, function(fgens, words, whicharg)
local fam, errmsg, onbreakmsg;

  onbreakmsg :=
      ["Type: 'quit;' to quit to outer loop, or",
       "type: 'words := <val>; return;' to assign <val> to words to continue.",
       "Note: fgens is the list of free group generators."];

  fam := FamilyObj(fgens[1]);
  errmsg := "words ";
  if whicharg <> "" then
    errmsg := Concatenation(errmsg, "(", whicharg, ") ");
  fi;
  while not IsList(words) or not ForAll(words, w -> FamilyObj(w) = fam) do
    if IsList(words) and ForAll(words, IsElementOfFreeGroup) then
      errmsg :=
        [Concatenation(
             errmsg, "is a list of words in the *wrong* free grp gen'rs")];
    elif IsList(words) and ForAll(words, IsElementOfFpGroup) then
      errmsg :=
        [Concatenation(
             errmsg, "must be a list of words in the free group gen'rs,"),
         "not fp group elements. Perhaps, you should use 'UnderlyingElement'",
         "to convert each fp group element to a word in the free group gen'rs"];
    else
      errmsg :=
        [Concatenation(
             errmsg, "must be a list of words in the free group gen'rs")];
    fi;
    Error(ACE_ERROR(errmsg, onbreakmsg), "\n");
  od;
  return Filtered(words, word -> not IsOne(word));
end);

#############################################################################
####
##
#F  PROCESS_ACE_OPTIONS . . . . . . . . . . . . . . . . .  Internal procedure
##  . . . . . . . . . for the ACE function with name ACEfname process options
##  . . . . . . . . . (on the top of OptionsStack)  with  names  newoptnames,
##  . . . . . . . . . other than those  that  are  fields  of  disallowed  or
##  . . . . . . . . . listed in ignored, by sending them to ACE via the write
##  . . . . . . . . . function ToACE,  after  appropriate  translation  where
##  . . . . . . . . . necessary, mostly in the order specified by  the  user.
##  . . . . . . . . . The list optnames contains the names of  all  currently
##  . . . . . . . . . active options i.e. the fields of all options on top of
##  . . . . . . . . . the OptionsStack. If  echo  is  set  then  all  options
##  . . . . . . . . . processed are echoed along with an  indication  of  how
##  . . . . . . . . . they were handled by the interface. If the InfoLevel of
##  . . . . . . . . . InfoACE or InfoWarning is at least 1 and the  user  has
##  . . . . . . . . . not passed the  acenowarnings  option  then  a  warning
##  . . . . . . . . . message is issued for each optname that is a  field  of
##  . . . . . . . . . disallowed or is in ignored or for some other reason is
##  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  ignored.
##
InstallGlobalFunction(PROCESS_ACE_OPTIONS,
function(ACEfname, optnames, newoptnames, echo, datarec, disallowed, ignored)
local ToACE, IsValidOptionValue, CheckValidOption, ProcessOption,
      AddIgnoreOptionsToIgnored, IsMyLine, nowarnings, ignoreunknown,
      paramoptnames, strategy, opt, optname, line, invokesEnumeration;

  ToACE := function(list)
    WRITE_LIST_TO_ACE_STREAM(datarec.stream, list);
  end;

  IsValidOptionValue := function(val)
    # Check that val is a valid value of opt.fullname.
    # This function will only be called when opt.known = true,
    # in which case, opt.fullname will be a field of KnownACEOptions
    if IsFunction(KnownACEOptions.(opt.fullname)[2]) then
      return KnownACEOptions.(opt.fullname)[2](val);
    elif IsBool(val) then
      return KnownACEOptions.(opt.fullname)[2] in ["", ["",0,1], [0,1]];
    else
      return val in KnownACEOptions.(opt.fullname)[2];
    fi;
  end;

  CheckValidOption := function(val)
    # If opt.fullname is a known allowed optname and val is a valid value,
    # warn the user of a possible error, if s/he wants to know and its
    # not an ignored option.
    if not(nowarnings or opt.ignore) then
      if opt.fullname in RecNames(disallowed) then
        Info(InfoACE + InfoWarning, 1,
             "ACE Warning: ", opt.name, ": ", disallowed.(opt.fullname));
      elif opt.known then
        if not IsValidOptionValue(val) then
          Info(InfoACE + InfoWarning, 1,
               "ACE Warning: ", val, ": ",
               "possibly not an allowed value of ", opt.name);
        fi;
      else
        Info(InfoACE + InfoWarning, 1,
             "ACE Warning: ", opt.name, ": unknown (maybe new) or bad option");
      fi;
    fi;
  end;

  ProcessOption := function(val)
    # Echo what we are about to do first, if the user has set the echo
    # option.
    if echo > 0 then
      if opt.ignore then
        Print(" ", opt.name, " := ", opt.value, " (ignored)\n");
      elif opt.fullname in RecNames(ACE_OPT_ACTIONS) then
        Print(" ", opt.name);
        if val = "" then
          Print(" (no value, ");
        else
          Print(" := ", opt.value, " (");
        fi;
        Print( ACE_OPT_ACTIONS.(opt.fullname), ")\n" );
      elif opt.fullname in NonACEbinOptions then
        Print(" ", opt.name, " := ", opt.value, " (not passed to ACE)\n");
      elif opt.list then
        Print(" ", opt.name, " := ", opt.value,
              " (brackets are not passed to ACE)\n");
      elif val = "" then
        Print(" ", opt.name, " (no value)\n");
      else
        Print(" ", opt.name, " := ", val, "\n");
      fi;
    fi;
    # Warn user if opt.name is an unknown optname or has an unexpected value
    # if they want to know.
    CheckValidOption(val);
    # Now do it ... pass opt.ace (which is opt.name except when the ACE and
    # GAP optnames differ) to ACE with value val,  except if opt.name is to
    # be ignored or is a NonACEbinOption without a translation.
    if not opt.donotpass and not opt.ignore then
      if opt.fullname in RecNames(ACE_OPT_TRANSLATIONS) then
        # The ACE optname differs from the GAP optname
        opt.ace := ACE_OPT_TRANSLATIONS.(opt.fullname);
      else
        # The ACE optname is the same as the GAP optname
        opt.ace := opt.name;
      fi;
      if opt.list then
        ToACE([ opt.ace,":",
                JoinStringsWithSeparator( List(val, String) ), ";" ]);
      elif val = "" then
        ToACE([ opt.ace, ";" ]);
      elif opt.fullname = "aceincomment" then
        ToACE([ opt.ace, val, ";" ]);
      else
        ToACE([ opt.ace, ":", val, ";" ]);
      fi;

      # Eventually we may include more general support for interpretation
      # of ACE output here ... for the moment we ensure the enumeration
      # result is set (for ACEStats) and the coset table is set (for
      # ACECosetTable[FromGensAndRels]) if there is an enumeration result
      if IsBound(datarec.procId) then
        if not IsBound( ACE_OPT_SENTINELS.(opt.synonyms[1]) ) then
          # Flush any available output ... it may contain errors
          line := ReadAllLine(datarec.stream);
          while line <> fail do
            Info(InfoACE + InfoWarning, 1, Chomp(line));
            line := ReadAllLine(datarec.stream);
          od;
        elif opt.fullname = "print" and IsBound(datarec.stats) and
             val in [ "", datarec.stats.activecosets ] and
             (datarec.stats.index <> 0 or
              VALUE_ACE_OPTION(optnames, false, "incomplete") ) then
          datarec.cosettable := ACE_COSET_TABLE(datarec.stats.activecosets,
                                                datarec.acegens,
                                                datarec.stream,
                                                ACE_READ_NEXT_LINE);
        else
          if ACE_OPT_SENTINELS.(opt.synonyms[1]) = fail then
            ToACE([ "text:***" ]);
            IsMyLine := line -> IsMatchingSublist(line, "***");
          else
            IsMyLine := ACE_OPT_SENTINELS.(opt.synonyms[1]);
          fi;
          invokesEnumeration := opt.synonyms[1] in
                                ["start", "continu", "redo", "aep", "rep"];
          repeat
            line := ACE_READ_NEXT_LINE(datarec.stream);
            if invokesEnumeration and
               not IsMatchingSublist(line, "** ERROR") and
               Length(line) > 1 and line[ Length(line) - 1 ] = ')' then
              datarec.enumResult := Chomp(line);
              datarec.stats := ACE_STATS(datarec.enumResult);
            fi;
            Info(InfoACE + InfoWarning, 1, Chomp(line));
          until IsMyLine(line);
        fi;
      fi;

    fi;
  end;

  AddIgnoreOptionsToIgnored := function()
  local ignore, optname, opt;
    ignore := VALUE_ACE_OPTION(optnames, [], "aceignore");
    for optname in ignore do
      opt := rec(name := optname);
      FULL_ACE_OPT_NAME(opt); # sets opt.known and opt.fullname
      Add(ignored, opt.fullname);
    od;
  end;

  if echo > 0 then
    Print(ACEfname, " called with the following options:\n");
    if echo = 2 then
      paramoptnames := RecNames(ACEParameterOptions);
      strategy := "default";
    fi;
  fi;

  nowarnings := VALUE_ACE_OPTION(optnames, false, "acenowarnings");
  ignoreunknown := VALUE_ACE_OPTION(optnames, ACEIgnoreUnknownDefault,
                                    "aceignoreunknown");
  AddIgnoreOptionsToIgnored();

  for optname in newoptnames do
    opt := ACEOptionData(optname); # sets opt.name, opt.known, opt.fullname
                                   # and opt.synonyms
    opt.value := ValueOption(opt.name);
    if echo = 2 then
      paramoptnames := Difference(paramoptnames, opt.synonyms);
      if opt.fullname in ACEStrategyOptions then
        strategy := opt.fullname;
        if IsInt(opt.value) then
          strategy := Concatenation(strategy, String(opt.value));
        elif opt.value and (opt.fullname = "felsch") then
          strategy := "felsch0";     # Hmm! I'd like to do this differently!!
        fi;
      fi;
    fi;
    # We don't pass the NonACEbinOptions options to ACE unless they
    # have a translation (i.e. are fields of ACE_OPT_TRANSLATIONS)
    opt.donotpass := (opt.fullname in NonACEbinOptions) and
                     not (opt.fullname in RecNames(ACE_OPT_TRANSLATIONS));
    opt.ignore := opt.fullname in RecNames(disallowed) or
                  opt.fullname in ignored or
                  (ignoreunknown and not opt.known);
    opt.list := false;
    if opt.value = true then
      # An option detected by GAP as boolean may in fact be a no-value
      # option of ACE ... unknown ACE options detected as being true are
      # assumed to be no-value options (since the user can still over-ride
      # this behaviour by entering values of 0 or 1 explicitly e.g.
      # ACEStats(... : `opt' := 1) )
      if not opt.known or IsValidOptionValue("") then
        ProcessOption("");
      else
        ProcessOption(1);
      fi;
    elif opt.value = false then
      ProcessOption(0);
    elif not IsString(opt.value) and IsList(opt.value) then
      opt.list := true;
      ProcessOption(opt.value);
    else
      ProcessOption(opt.value);
    fi;
  od;

  if echo = 2 then
    Print("Other options set via ACE defaults:\n");
    for optname in paramoptnames do
      Print(" ", optname, " := ");
      if IsRecord(ACEParameterOptions.(optname)) then
        if IsBound(ACEParameterOptions.(optname).(strategy)) then
          Print(ACEParameterOptions.(optname).(strategy), "\n");
        else
          Print(ACEParameterOptions.(optname).default, "\n");
        fi;
      else
        Print(ACEParameterOptions.(optname), "\n");
      fi;
    od;
  fi;

end);

#############################################################################
####
##
#F  PROCESS_ACE_OPTION  . . . . . . . . . . . . . . . . .  Internal procedure
##  . . . . . . . . . . . . . . . . . . . . . process  a  single  ACE  option
##  . . . . . . . . . . . . . . . . . . . . . that  hasn't  been  passed  via
##  . . . . . . . . . . . . . . . . . . . . . . . . .  GAP's option mechanism
##
##  Checks optval is a valid value of optname (which must  be  lowercase  and
##  unabbreviated) and pass it ACE by writing to stream.
##
InstallGlobalFunction(PROCESS_ACE_OPTION, function(stream, optname, optval)
local aceoptname, error;

  # Check that optval is a valid value of optname.
  if IsFunction(KnownACEOptions.(optname)[2]) then
    error := not KnownACEOptions.(optname)[2](optval);
  else
    error := not (optval in KnownACEOptions.(optname)[2]);
  fi;

  if error then
    Info(InfoACE + InfoWarning, 1,
         "ACE Warning: ", optval, ": ",
         "possibly not an allowed value of ", optname);
  fi;

  if optname in RecNames(ACE_OPT_TRANSLATIONS) then
    # The ACE optname differs from the GAP optname
    aceoptname := ACE_OPT_TRANSLATIONS.(optname);
  else
    # The ACE optname is the same as the GAP optname
    aceoptname := optname;
  fi;

  if optval = "" then
    WRITE_LIST_TO_ACE_STREAM(stream, [ aceoptname, ";" ]);
  elif not IsString(optval) and IsList(optval) then
    WRITE_LIST_TO_ACE_STREAM(
        stream, [ aceoptname,":",
                  JoinStringsWithSeparator( List(optval, String) ), ";" ]
        );
  else
    WRITE_LIST_TO_ACE_STREAM(stream, [ aceoptname, ":", optval, ";" ]);
  fi;

  return error;
end);

#############################################################################
####
##
#F  ACEOptionData . . .  returns a record of the known data of an option name
##
##  For argument optname the fields of the returned record are:
##    name  . . . .  optname (unchanged);
##    known . . . .  true iff optname is a valid mixed case abbreviation of a
##                   KnownACEOption field;
##    fullname  . .  the lower case unabbreviated  form  of  optname  if  the
##                   `known' field is set `true',  or optname in  lower case,
##                   otherwise;
##    synonyms  . .  a list of KnownACEOptions fields that are  option  names
##                   synonymous with optname, if the  `known'  field  is  set
##                   set `true', or list with just fullname otherwise;
##    abbrev  . . .  the shortest lowercase abbreviation of  optname  if  the
##                   `known' field is set `true', or fullname otherwise.
##
InstallGlobalFunction(ACEOptionData, function(optname)
local opt;
  opt := rec(name := optname);
  FULL_ACE_OPT_NAME(opt); # Sets the `known' and `fullname' fields
  if opt.known then
    opt.synonyms := ACE_OPTION_SYNONYMS(opt.fullname);
    opt.abbrev := opt.fullname{[1 ..  KnownACEOptions.(opt.fullname)[1]]};
  else
    opt.synonyms := [ opt.fullname ];
    opt.abbrev := opt.fullname;
  fi;
  return opt;
end);

#############################################################################
####
##
#F  SANITISE_ACE_OPTIONS  . . . . . . . . . . . . . . . .  Internal procedure
##  . . . . . . . . . . . . . . . . . . . . . . . .  Called by SetACEOptions,
##  . . . . . . . . . . . . . . . . . or by CALL_ACE when CALL_ACE is invoked
##  . . . . . . . . . . . . . . . . . by   ACEExample   with   user   options
##
##  Scrubs any option  names  in  optsrec  that match  those  in  newoptsrec,
##  to ensure that *all* new options are at the end of  optsrec  when  it  is
##  updated with options from newoptsrec.
##
InstallGlobalFunction(SANITISE_ACE_OPTIONS, function(optsrec, newoptsrec)
local newoptnames, optname, opt;
    newoptnames := Concatenation(
                       List(RecNames(newoptsrec),
                            optname -> ACEOptionData(optname).synonyms)
                       );
    for optname in RecNames(optsrec) do
      opt := rec(name := optname);
      FULL_ACE_OPT_NAME(opt); # Sets opt.fullname
      if opt.fullname in newoptnames then
        Unbind(optsrec.(optname));
      fi;
    od;
end);

#############################################################################
####
##
#F  NEW_ACE_OPTIONS()
##
##  Looks at OptionsStack and returns the new options.
##
InstallGlobalFunction(NEW_ACE_OPTIONS, function()
local newoptions, oldoptions, oldnames, optname;
    if IsEmpty(OptionsStack) then
      return rec();
    elif Length(OptionsStack) = 1 then
      return OptionsStack[ Length(OptionsStack) ];
    else
      newoptions := ShallowCopy( OptionsStack[ Length(OptionsStack) ] );
      oldoptions := OptionsStack[ Length(OptionsStack) - 1 ];
      oldnames := RecNames(oldoptions);
      for optname in RecNames(newoptions) do
        if optname in oldnames and
           oldoptions.(optname) = newoptions.(optname) then
          Unbind( newoptions.(optname) );
        fi;
      od;
      return newoptions;
    fi;
end);

#E  options.gi  . . . . . . . . . . . . . . . . . . . . . . . . . . ends here


#
#
#

Read("~/Workspace/Chevalley.gap/init.gi");

Read(Filename(home_dir,"lib/rsys.gd"));
Read(Filename(home_dir,"lib/rsys.gi"));

Read(Filename(home_dir,"lib/chvadj.gd"));
Read(Filename(home_dir,"lib/chvadj.gi"));

Read(Filename(home_dir,"lib/nilchv.gd"));
Read(Filename(home_dir,"lib/nilchv.gi"));


############################################################################
##
##  access.gd                     IRREDSOL                  Burkhard Höfling
##
##  Copyright © 2003–2016 Burkhard Höfling
##

############################################################################
##
#F  IndicesAbsolutelyIrreducibleSolubleMatrixGroups(<n>, <q>)
##
##  see the IRREDSOL manual
##
InstallGlobalFunction(IndicesAbsolutelyIrreducibleSolubleMatrixGroups,
    function(n, q)
        Info(InfoWarning, 1, "Obsolete function. See ?IndicesAbsolutelyIrreducibleSolubleMatrixGroups.");
        return IndicesIrreducibleSolubleMatrixGroups(n, q, 1);
    end);

############################################################################
##
#F  AbsolutelyIrreducibleSolubleMatrixGroup(<n>, <q>, <k>)
##
##  see the IRREDSOL manual
##
InstallGlobalFunction(AbsolutelyIrreducibleSolubleMatrixGroup,
    function(n, q, k)
        Info(InfoWarning, 1, "Obsolete function. See ? AbsolutelyIrreducibleSolubleMatrixGroup.");
        return IrreducibleSolubleMatrixGroup(n, q, 1, k);
    end);

############################################################################
##
#F  RecognitionAbsolutelyIrreducibleSolubleMatrixGroup(G, wantmat, wantgroup)
##
##  see the IRREDSOL manual
##
InstallGlobalFunction(RecognitionAbsolutelyIrreducibleSolubleMatrixGroup,
    function(G, wantmat, wantgroup)
        local r;
        Info(InfoWarning, 1, "Obsolete function. See ? RecognitionAbsolutelyIrreducibleSolubleMatrixGroup.");
        r := RecognitionIrreducibleSolubleMatrixGroup(G, wantmat, wantgroup);
        if r.id[3] <> 1 then
            Error("G is not absolutely irreducible");
        fi;
        r.id := r.id{[1,2,4]};
        return r;
    end);

############################################################################
##
#F  RecognitionAbsolutelyIrreducibleSolubleMatrixGroupNC(G, wantmat, wantgroup)
##
##  see the IRREDSOL manual
##
InstallGlobalFunction(RecognitionAbsolutelyIrreducibleSolubleMatrixGroupNC,
    function(G, wantmat, wantgroup)
        local r;
        Info(InfoWarning, 1, "Obsolete function. See ? RecognitionAbsolutelyIrreducibleSolubleMatrixGroupNC.");
        r := RecognitionIrreducibleSolubleMatrixGroupNC(G, wantmat, wantgroup);
        if r <> fail then
            if r.id[3] <> 1 then
                Error("G is not absolutely irreducible");
            fi;
            r.id := r.id{[1,2,4]};
        fi;
        return r;
    end);

############################################################################
##
#A  IdAbsolutelyIrreducibleSolubleMatrixGroup(<G>)
##
##  see the IRREDSOL manual
##
InstallGlobalFunction("IdAbsolutelyIrreducibleSolubleMatrixGroup",
    function(G)
        local r;
        Info(InfoWarning, 1, "Obsolete function. See ? IdAbsolutelyIrreducibleSolubleMatrixGroup.");
        r := IdIrreducibleSolubleMatrixGroup(G, false, false);
        if r[3] <> 1 then
            Error("G is not absolutely irreducible");
        fi;
        return r{[1,2,4]};
    end);

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


# Copyright (c) 2018-2021, Carnegie Mellon University
# See LICENSE for details

#
#######################
#
# local helper functions
#
#######################

#
## _fProto
#
# create a TFunc(...) object from an array of variables
#
# eg: _fProto([
#        var.fresh_t("i", TPtr(TInt)),
#        var.fresh_t("b", TReal)
#     ])
#     ----> TFunc( TPtr(TInt), TReal )
#
# NOTE: useful for converting lists of fully typed variables into a function proto

_fProto := (a) -> ApplyFunc(TFunc, List(a, e -> e.t));

#
## _fCall
#
# create a function call in 'spiral-code' reprentation given an array and a fully typed variable list
#
# eg: _fCall("foo", [
#        var.fresh_t("i", TPtr(TInt)),
#        var.fresh_t("b", TReal)
#     ])
#     ----> call("foo", TFunc(TPtr(TInt), TReal), i, b)
#
# NOTE: this is generally unparsed to
#
#     foo(i, b)
#
_fCall := (name, var_array) -> ApplyFunc(call, Concat([var(name, _fProto(var_array))], var_array));

#
## _wrapInPtr
#
# copy a list of vars, creating new vars that are pointers
#
# eg: _wrapInPtr([
#        var.fresh_t("i", TPtr(TInt))
#        var.fresh_t("b", TReal)
#     ])
#     ---> [ var.fresh_t("i", TPtr(TPtr(TInt))), var.fresh_t("b", TPtr(TReal)) ]
#
# this is useful if you want to pass by reference, as in the case of allocation functions.
#
# NOTE: this function does not modify the input array

_wrapInPtr := (var_array) -> List(var_array, e -> CopyFields(e, rec(t := TPtr(e.t))));

#
## _arraysToPtrs
#
# turns arrays into pointers so that the arrays are not statically defined.
#
_arraysToPtrs := (var_array) -> List(var_array,
    e -> When(ObjId(e.t) = TArray,
        CopyFields(e, rec(t := TPtr(e.t.t))),
        CopyFields(e)
    )
);

#
## _getXY
#
# get input and output arrays. returns a nested array,
#   IsArray(X) and IsArray(Y) = true
#
# NOTE: var.fresh cannot be used here, as this function needs to ALWAYS
# return the same data, as it may be called >1
#
_getXY := function(sums, opts)
    local X, Y, precision;

    precision := 64;

    if IsBound(opts.precision) then
        if opts.precision = "single" then
            precision := 32;
        elif opts.precision = "double" then
            precision := 64;
        else
            Error("Unknown precision");
        fi;
    fi;

    if IsBound(opts.doSumsUnification) and opts.doSumsUnification then
        X := List([1..Length(sums.dmn())],
           (i) -> var(Concatenation("X", String(i)), sums.dmn()[i], sums.dmn()[i].size));
        Y := List([1..Length(sums.rng())],
           (i) -> var(Concatenation("Y", String(i)), sums.rng()[i], sums.rng()[i].size));

        return [X, Y];
    fi;

    if IsList(sums.dims()[2])  then
        X := List([ 1 .. Length(sums.dims()[2]) ],
            (x) -> var(Concatenation("X", String(x)), TPtr(T_Real(precision)), sums.dims()[2]));
    else
        X := Cond(IsBound(opts.X), opts.X, [var("X", TArray(T_Real(precision), sums.dims()[2]), sums.dims()[2])]);
    fi;

    if IsList(sums.dims()[1])  then
        Y := List([ 1 .. Length(sums.dims()[1]) ],
            (x) -> var(Concatenation("Y", String(x)), TPtr(T_Real(precision)), sums.dims()[1]));
    else
        Y := Cond(
            opts.inplace, X,
            IsBound(opts.Y), opts.Y,
            [var("Y", TArray(T_Real(precision), sums.dims()[1]), sums.dims()[1])]
        );
    fi;

    return [X, Y];
end;

######################
#
## default wrappers: init/compute, alloc, timer, verify, data
#
# these defaults generate the above functions and our output has the following
# characteristics:
#
# temp variables on the stack
# statically declared data (twiddles in case of DFT)
# calloc'ed (default allocate func) input/output pointers
#
######################

#
## _DefaultWrapInitCompute
#
#
# wraps passed in code in compute() function, generates init() function.
#
Class(_DefaultWrapInitCompute, CodegenStrat, rec(
    __call__ := function(code, sums, opts)
        local x, y, io, params, datas, sub, initsub, init, compute;

        # extract data from 'sums'
        [x, y] := _getXY(sums, opts);
        io := When(x=y, x, Concat(y, x));
        params := Set(Collect(sums, param));
        datas := Collect(sums, FDataOfs);

        # names
        sub := Cond(IsBound(opts.subName), opts.subName, "compute");
        initsub := Cond(IsBound(opts.subName), Concat("init_", opts.subName), "initz");

        # generate the 'init' code
        if IsBound(opts.generateInitFunc) and opts.generateInitFunc then
            init := func(TVoid, initsub, params, chain(
                List(datas, e -> SReduce(e.var.init, opts))
            ));
        else
            init := func(TVoid, initsub, When(params = [], [TVoid], params), code);
        fi;

        # wrap the 'compute'
        compute := func(TVoid, sub, Concatenation(io, params), code);

        return program(chain(
            init,
            compute
        ));
    end
));

#
## _DefaultWrapAlloc
#
# sets up allocation functions.
#
# NOTE: only functions in 'code' are preserved, everything else is trashed.

Class(_DefaultWrapAlloc, CodegenStrat, rec(
    __call__ := function(code, sums, opts)
        local x, y, io, iop, sub, initsub, funcs, alloc, free;

        [x, y] := _getXY(sums, opts);
        io := When(x=y, x, Concat(y, x));
        iop := _wrapInPtr(io);

        # names
        sub := Cond(IsBound(opts.subName), opts.subName, "compute");
        initsub := Cond(IsBound(opts.subName), Concat("init_", opts.subName), "initz");

        # extract functions.
        funcs := Collect(code, func);

        # ensure ordering, code must already be wrapped by init/compute
        Constraint(2 = Length(Filtered(funcs, e -> e.id = sub or e.id = initsub)));

        # we expect a program wrapper
        Constraint(ObjId(code) = program);

        alloc := func(TVoid, "alloc", iop, chain(
            List(iop, e -> allocate(
                deref(e),
                e.t.t
#                When(IsBound(e.range) and ObjId(e.t) = TPtr,
#                    TArray(e.t.t, e.range),
#                    e.t
#                )
            ))
        ));

        free := func(TVoid, "dealloc", io, chain(
            List(io, e -> deallocate(
                e,
                e.t
#                When(IsBound(e.range) and ObjId(e.t) = TPtr,
#                    TArray(e.t.t, e.range),
#                    e.t
#                )
            ))
        ));

#        alloc.countedArithCost := (self, countrec) >> countrec.arithcost(0);
#        free.countedArithCost := (self, countrec) >> countrec.arithcost(0);

        Add(code.cmds, alloc);
        Add(code.cmds, free);

        return code;
    end
));

#
## _DefaultWrapTimer
#
# adds a timer function.
#
# relies on existing init, compute, alloc, and dealloc.

Class(_DefaultWrapTimer, CodegenStrat, rec(
    __call__ := function(code, sums, opts)
        local x, y, io, params, sub, initsub, funcs, i, numruns, t, timer;

        [x, y] := _getXY(sums, opts);
        io := When(x=y, x, Concat(y, x));
        params := Set(Collect(sums, param));

        # names
        sub := Cond(IsBound(opts.subName), opts.subName, "compute");
        initsub := Cond(IsBound(opts.subName), Concat("init_", opts.subName), "initz");

        # extract functions.
        funcs := Collect(code, func);

        # ensure ordering, make sure we have an init and compute
        Constraint(2 = Length(Filtered(funcs, e -> e.id = sub or e.id = initsub)));

        # ensure ordering, make sure we have the alloc/dealloc functions too!
        Constraint(2 = Length(Filtered(funcs, e -> e.id = "alloc" or e.id = "dealloc")));

        # we expect a program wrapper
        Constraint(ObjId(code) = program);

        # these two are used interchangably, MUST be same type.
        i := var.fresh_t("i", TInt);
        numruns := var.fresh_t("numruns", TInt);

        t := var("t", TPtr(TVoid));

        timer := func(TVoid, "timer", [t, numruns],
            decl(Concat(_arraysToPtrs(io), params), chain(
                _fCall("alloc", List(io, addrof)),
                _fCall(initsub, params),

                # this is used by simics to switch from a fast functional
                # to a slow timed execute mode
                When(IsBound(opts.extraTimerCall) and opts.extraTimerCall,
                    _fCall("timer_start", [t]),
                    skip()
                ),
                When(IsBound(opts.coldcache) and opts.coldcache,
                    skip(),
                    _fCall(sub, Concat(io, params)) # warm up the cache by default
                ),
                _fCall("timer_start", [t]),
                loop(i, numruns,
                    _fCall(sub, Concat(io, params))
                ),
                _fCall("timer_end", [t]),
                _fCall("dealloc", io)
            ))
        );

#        timer.countedArithCost := (self, countrec) >> countrec.arithcost(0);

        Add(code.cmds, timer);

        return code;
    end,
));

#
## _DefaultWrapVerify
#
#
Class(_DefaultWrapVerify, CodegenStrat, rec(
    __call__ := function(code, sums, opts)
        local x, y, io, params, sub, initsub, funcs, i, j, basis, printY, verify;

        [x, y] := _getXY(sums, opts);
        io := When(x=y, x, Concat(y, x));
        params := Set(Collect(sums, param));

        # names
        sub := Cond(IsBound(opts.subName), opts.subName, "compute");
        initsub := Cond(IsBound(opts.subName), Concat("init_", opts.subName), "initz");

        # extract functions.
        funcs := Collect(code, func);

        # ensure ordering, make sure we have an init and compute
        Constraint(2 = Length(Filtered(funcs, e -> e.id = sub or e.id = initsub)));

        # ensure ordering, make sure we have the alloc/dealloc functions too!
        Constraint(2 = Length(Filtered(funcs, e -> e.id = "alloc" or e.id = "dealloc")));

        # we expect a program wrapper
        Constraint(ObjId(code) = program);

        i := var.fresh_t("i", TInt);
        j := var.fresh_t("j", TInt);

        basis := func(TVoid, "basis", Concat(x, [j]),
            chain(
                loop(i, x[1].range,
                    assign(nth(x[1], i), V(0))
                ),
                assign(nth(x[1], j), V(1))
            )
        );

        # outputs only entries in y[1].
        printY := func(TVoid, "printY", y, chain(
            PRINT("["),
            PRINT("%lf", nth(y[1], 0)),
            loop(i, (y[1].range-1),
                PRINT(", %lf", nth(y[1], add(i,1)))
            ),
            PRINT("]")

        ));

        verify := func(TVoid, "verify", [],
            decl(_arraysToPtrs(io), chain(

                _fCall("alloc", List(io, addrof)),

                _fCall(initsub, params),

                PRINT("[\\n"),
                loop(i, x[1].range, chain(
                    _fCall("basis", Concat(x, [i])),
                    _fCall(sub, Concat(io, params)),
                    _fCall("printY", y),

                    IF(neq(i, x[1].range-1), PRINT(",\\n"), skip())
                )),
                PRINT("\\n];\\n"),
                _fCall("dealloc", io)
            ))
        );

#        basis.countedArithCost := (self, countrec) >> countrec.arithcost(0);
#        printY.countedArithCost := (self, countrec) >> countrec.arithcost(0);
#        verify.countedArithCost := (self, countrec) >> countrec.arithcost(0);

        Append(code.cmds, [basis, printY, verify]);

        return code;
    end
));

#
## _DefaultWrapData
#
# must be done after all function wraps
#
# allocates data arrays (for things like twiddles) globally in the file.
#
Class(_DefaultWrapData, CodegenStrat, rec(
    __call__ := function(code, sums, opts)
        local datas, data;

        datas := Collect(sums, FDataOfs);

        # for static data declared in file.
        data := List(datas, e -> e.var);

        # we expect a program wrapper
        Constraint(ObjId(code) = program);

        # put data chunk at start of program

        code.cmds := Concat([decl(data, skip())], code.cmds);

        return code;
    end
));

#
## _DefaultWrapAll
#
# same structure as the new legacy wrap except uses the new timer paradigm.
#
# by structure, we have
# data (like twiddles) in the data segment, not allocated
# input/output arrays allocated
# temporary arrays on the stack
#
Class(_DefaultWrap, CodegenStrat, rec(
    __call__ := function(code, sums, opts)
        local
            x, y, io, iop, params, datas,                # sums related data
            sub, initsub,                                # strings
            t, j, i,                                   # var names
            data,                                        # twiddle/other data
            init, compute, alloc, free, timer, verify,   # functions
            basis, printY,                               #
            prog;                                        # full program

        #
        # data from sums
        #

        # in/out and sizes
        [x, y] := _getXY(sums, opts);
        io := When(x=y, x, Concat(y, x));
        iop := _wrapInPtr(io);
        params := Set(Collect(sums, param));
        datas := Collect(sums, FDataOfs);

        # names
        sub := Cond(IsBound(opts.subName), opts.subName, "compute");
        initsub := Cond(IsBound(opts.subName), Concat("init_", opts.subName), "initz");

        #
        # code sections start here:
        #

        # generate the 'init' code
        if IsBound(opts.generateInitFunc) and opts.generateInitFunc then
            init := func(TVoid, initsub, params, chain(
                List(datas, e -> SReduce(e.var.init, opts))
            ));
        else
            init := func(TVoid, initsub, params, code);
        fi;

        # wrap the 'compute'
        compute := func(TVoid, sub, Concatenation(io, params), code);

        # create the allocs/free
        #
        # NOTE: extra level of derefs because we modify incoming pointers
        alloc := func(TVoid, "alloc", iop, chain(
            List(iop, e -> allocate(
                deref(e),
                When(IsBound(e.range) and ObjId(e.t.t) = TPtr,
                    TArray(e.t.t.t, e.range),
                    e.t.t
                )
            ))
        ));

        free := func(TVoid, "dealloc", io, chain(
            List(io, e -> deallocate(
                e,
                When(IsBound(e.range) and ObjId(e.t) = TPtr,
                    TArray(e.t.t, e.range),
                    e.t
                )
            ))
        ));

        #
        # setup timer
        #

        # these two are used interchangably, MUST be same type.
        j := var.fresh_t("j", TInt);
        i := var.fresh_t("i", TInt);

        t := var("t", TPtr(TVoid));

        timer := func(TVoid, "timer", [t, j],
            decl(Concat(io, params), chain(
                _fCall("alloc", List(io, addrof)),
                _fCall(initsub, params),
                _fCall("timer_start", [t]),
                loop(i, j,
                    _fCall(sub, Concat(io, params))
                ),
                _fCall("timer_end", [t]),
                _fCall("dealloc", io)
            ))
        );

        #
        # verifier
        #

        # verifier helpers
        #
        #

        basis := func(TVoid, "basis", Concat(x, [j]),
            chain(
                loop(i, x[1].range,
                    assign(nth(x[1], i), V(0))
                ),
                assign(nth(x[1], j), V(1))
            )
        );

        printY := func(TVoid, "printY", y, skip());

        verify := func(TVoid, "verify", [],
            decl(io, chain(

                _fCall("alloc", List(io, addrof)),

                _fCall(initsub, params),

                loop(i, x[1].range, chain(
                    _fCall("basis", Concat(x, [i])),
                    _fCall(sub, Concat(io, params)),
                    _fCall("printY", y)
                )),

                _fCall("dealloc", io)
            ))
        );

        # for static data declared in file.
        data := List(datas, e -> e.var);

        #
        # put pieces together
        #

        prog := program(
            decl(data, chain(
                init,
                compute,
                comment(""),
                comment("****************************************"),
                comment("* timer/verifier code below this point *"),
                comment("****************************************"),
                alloc,
                free,
                timer,
                basis,
                printY,
                verify
            ))
        );

        return prog;
    end,
));


~% gap

            #########           ######         ###########           ###
         #############          ######         ############         ####
        ##############         ########        #############       #####
       ###############         ########        #####   ######      #####
      ######         #         #########       #####    #####     ######
     ######                   ##########       #####    #####    #######
     #####                    ##### ####       #####   ######   ########
     ####                    #####  #####      #############   ###  ####
     #####     #######       ####    ####      ###########    ####  ####
     #####     #######      #####    #####     ######        ####   ####
     #####     #######      #####    #####     #####         #############
      #####      #####     ################    #####         #############
      ######     #####     ################    #####         #############
      ################    ##################   #####                ####
       ###############    #####        #####   #####                ####
         #############    #####        #####   #####                ####
          #########      #####          #####  #####                ####

     Information at:  http://www.gap-system.org
     Try '?help' for help. See also  '?copyright' and  '?authors'

   Loading the library. Please be patient, this may take a while.
GAP4, Version: 4.4.12 of 17-Dec-2008, x86_64-unknown-linux-gnu-gcc
Components:  small 2.1, small2 2.0, small3 2.0, small4 1.0, small5 1.0, small6 1.0, small7 1.0, small8 1.0,
             small9 1.0, small10 0.2, id2 3.0, id3 2.1, id4 1.0, id5 1.0, id6 1.0, id9 1.0, id10 0.1, trans 1.0,
             prim 2.1  loaded.
Packages:    AClib 1.1, Polycyclic 2.6, Alnuth 2.2.5, AutPGrp 1.4, CrystCat 1.1.3, Cryst 4.1.6, CRISP 1.3.2,
             CTblLib 1.1.3, TomLib 1.1.4, FactInt 1.5.2, GAPDoc 1.2, FGA 1.1.0.1, IRREDSOL 1.1.2, LAGUNA 3.5.0,
             Sophus 1.23, Polenta 1.2.7, ResClasses 2.5.3  loaded.
gap> join := function(a, b, sep)
>   return Concatenation(a, sep, sep, b);
> end;
function( a, b, sep ) ... end
gap>
gap> join("Rosetta", "Code", ":");
"Rosetta::Code"
gap>