#############################################################################
##
#W  selfsimgroup.gi           automgrp package                 Yevgen Muntyan
#W                                                             Dmytro Savchuk
##
#Y  Copyright (C) 2003 - 2018 Yevgen Muntyan, Dmytro Savchuk
##

###############################################################################
##
#M  SelfSimilarGroup(<list>)
##
InstallMethod(SelfSimilarGroup, "for [IsList]", [IsList],
function(list)
  return SelfSimilarGroup(list, false);
end);

###############################################################################
##
#M  SelfSimilarGroup(<list>, <bind_vars>)
##
InstallMethod(SelfSimilarGroup, "for [IsList, IsBool]", [IsList, IsBool],
function(list, bind_vars)
  if not AG_IsCorrectRecurList(list, true) then
    Error("in SelfSimilarGroup(IsList, IsBool):\n",
          "  given list is not a correct list representing self-similar group\n");
  fi;

  return GroupOfSelfSimFamily(SelfSimFamily(list, bind_vars));
end);

###############################################################################
##
#M  SelfSimilarGroup(<list>, <names>)
##
InstallMethod(SelfSimilarGroup, "for [IsList, IsList]", [IsList, IsList],
function(list, names)
  return SelfSimilarGroup(list, names, AG_Globals.bind_vars_autom_family);
end);

###############################################################################
##
#M  SelfSimilarGroup(<list>, <names>, <bind_vars>)
##
InstallMethod(SelfSimilarGroup,
              "for [IsList, IsList, IsBool]", [IsList, IsList, IsBool],
function(list, names, bind_vars)
  if not AG_IsCorrectRecurList(list, true) then
    Error("error in SelfSimilarGroup(IsList, IsList, IsBool):\n",
          "  given list is not a correct list representing self-similar group\n");
  fi;

  return GroupOfSelfSimFamily(SelfSimFamily(list, names, bind_vars));
end);

###############################################################################
##
#M  SelfSimilarGroup(<string>)
#M  SelfSimilarGroup(<string>, <bind_vars>)
##
InstallMethod(SelfSimilarGroup, "for [IsString]", [IsString],
function(string)
  return SelfSimilarGroup(string, AG_Globals.bind_vars_autom_family);
end);

InstallMethod(SelfSimilarGroup, "for [IsString, IsBool]", [IsString, IsBool],
function(string, bind_vars)
  local s;
  s := AG_ParseAutomatonStringFR(string);
  return SelfSimilarGroup(s[2], s[1], bind_vars);
end);

###############################################################################
##
#M  SelfSimilarGroup(<A>)
#M  SelfSimilarGroup(<A>, <bind_vars>)
##
InstallMethod(SelfSimilarGroup, "for [IsMealyAutomaton]", [IsMealyAutomaton],
function(A)
  if not IsInvertible(A) then
    Error("Automaton <A> is not invertible");
  fi;
  return SelfSimilarGroup(AutomatonList(A), A!.states);
end);

InstallMethod(SelfSimilarGroup, "for [IsMealyAutomaton, IsBool]", [IsMealyAutomaton, IsBool],
function(A, bind_vars)
  if not IsInvertible(A) then
    Error("Automaton <A> is not invertible");
  fi;
  return SelfSimilarGroup(AutomatonList(A), A!.states, bind_vars);
end);

###############################################################################
##
#M  GroupOfSelfSimFamily(<G>)
##
InstallMethod(GroupOfSelfSimFamily, "for [IsSelfSimGroup]",
                   [IsSelfSimGroup],
function(G)
  return GroupOfSelfSimFamily(UnderlyingSelfSimFamily(G));
end);

###############################################################################
##
#M  IsGroupOfSelfSimFamily(<G>)
##
InstallMethod(IsGroupOfSelfSimFamily, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  return G = GroupOfSelfSimFamily(G);
end);

###############################################################################
##
#M  UseSubsetRelation(<G>)
##
InstallMethod(UseSubsetRelation,
              "for [IsSelfSimGroup, IsSelfSimGroup]",
              [IsSelfSimGroup, IsSelfSimGroup],
function(super, sub)
  ## the full group is self similar, so if <super> is smaller than the full
  ##  group then sub is smaller either
  if HasIsGroupOfSelfSimFamily(super) then
    if not IsGroupOfSelfSimFamily(super) then
      SetIsGroupOfSelfSimFamily(sub, false); fi; fi;
  TryNextMethod();
end);

###############################################################################
##
#M  __AG_SubgroupOnLevel(<G>, <gens>, <level>)
##
InstallMethod(__AG_SubgroupOnLevel, [IsSelfSimGroup,
                                    IsList and IsTreeAutomorphismCollection,
                                    IsPosInt],
function(G, gens, level)
  local overgroup;

  if IsEmpty(gens) or (Length(gens) = 1 and IsOne(gens[1])) then
    return TrivialSubgroup(G);
  fi;

  if HasIsGroupOfSelfSimFamily(G) and IsGroupOfSelfSimFamily(G) then
    overgroup := G;
  else
    overgroup := GroupOfSelfSimFamily(UnderlyingSelfSimFamily(G));
  fi;

  return SubgroupNC(overgroup, gens);
end);

InstallOtherMethod(__AG_SubgroupOnLevel, [IsSelfSimGroup, IsList and IsEmpty, IsPosInt],
function(G, gens, level)
  return TrivialSubgroup(G);
end);

InstallMethod(__AG_SubgroupOnLevel, [IsTreeAutomorphismGroup,
                                    IsList and IsSelfSimCollection,
                                    IsPosInt],
function(G, gens, level)
  local overgroup;

  overgroup := GroupOfSelfSimFamily(FamilyObj(gens[1]));

  if Length(gens) = 1 and IsOne(gens[1]) then
    return TrivialSubgroup(overgroup);
  fi;

  return SubgroupNC(overgroup, gens);
end);

InstallMethod(__AG_SimplifyGroupGenerators, "for [IsList and IsInvertibleSelfSimCollection]",
                          [IsList and IsInvertibleSelfSimCollection],
function(gens)
  local words, fam;

  if IsEmpty(gens) then
    return [];
  fi;

  fam := FamilyObj(gens[1]);
  words := FreeGeneratorsOfGroup(Group(List(gens, a -> a!.word)));

  if fam!.use_rws and not IsEmpty(words) then
    words := AG_ReducedForm(fam!.rws, words);
    if IsEmpty(words) then
      return [];
    fi;
    words := FreeGeneratorsOfGroup(Group(words));
  fi;

  return List(words, w -> SelfSim(w, fam));
end);

###############################################################################
##
#M  PrintObj(<G>)
##
InstallMethod(PrintObj, "for [IsSelfSimilarGroup]",
              [IsSelfSimilarGroup],
function(G)
  Print("SelfSimilarGroup(\"", String(G), "\")");
end);

###############################################################################
##
#M  Display(<G>)
##
InstallMethod(Display, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  local i, gens, printone;

  printone := function(a)
    Print(a, " = ", Decompose(a));
  end;

  gens := GeneratorsOfGroup(G);
  if gens = [] then Print("< >"); fi;
  if Length(gens) = 1 then
    Print("< "); printone(gens[1]); Print(" >");
  else
    Print("< "); printone(gens[1]); Print(", \n");
    for i in [2..Length(gens)-1] do
      Print("  "); printone(gens[i]); Print(", \n");
    od;
    Print("  "); printone(gens[Length(gens)]); Print(" >");
  fi;
end);

#############################################################################
##
#M  String(<G>)
##
InstallMethod(String, "for [IsSelfSimGroup]", [IsSelfSimGroup],
function(G)
  local i, gens, formatone, s;

  formatone := function(a)
    return Concatenation(String(a), " = ", String(Decompose(a)));
  end;

  gens := GeneratorsOfGroup(G);

  s := "";
  for i in [1..Length(gens)] do
    Append(s, formatone(gens[i]));
    if i <> Length(gens) then
      Append(s, ", ");
    fi;
  od;

  return s;
end);

###############################################################################
##
#M  ViewObj(<G>)
##
InstallMethod(ViewObj, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  local i, gens;
  gens := List(GeneratorsOfGroup(G), g -> Word(g));
  if gens = [] then Print("< >"); fi;
  Print("< ");
  for i in [1..Length(gens)-1] do
    if IsOne(gens[i]) then
      Print(AG_Globals.identity_symbol, ", ");
    else
      Print(gens[i], ", ");
    fi;
  od;
  if IsOne(gens[Length(gens)]) then
    Print(AG_Globals.identity_symbol, " >");
  else
    Print(gens[Length(gens)], " >");
  fi;
end);

###############################################################################
##
#M  IsFractalByWords(G)
##
InstallMethod(IsFractalByWords, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function (G)
  local freegens, stab, i, sym, f;

  sym := GroupWithGenerators(List(GeneratorsOfGroup(G), g -> Perm(g)));
  if not IsTransitive(sym, [1..DegreeOfTree(G)]) then
    Info(InfoAutomGrp, 1, "group is not transitive on first level");
    return false;
  fi;

  f := GroupWithGenerators(List(GeneratorsOfGroup(G), g -> Word(g)));
  stab := StabilizerOfFirstLevel(G);
  stab := List(GeneratorsOfGroup(stab), a -> StatesWords(a));

  for i in [1..DegreeOfTree(G)] do
    if f <> GroupWithGenerators(List(stab, s -> s[i])) then
      return false;
    fi;
  od;
  return true;
end);

###############################################################################
##
#M  Size(G)
##
InstallMethod(Size, "for [IsSelfSimGroup]", [IsSelfSimGroup],
function (G)
  local f;
  if IsTrivial(G) then
    Info(InfoAutomGrp, 3, "Size(G): 1, G is trivial");
    return 1;
  fi;

  if CanEasilyTestSphericalTransitivity(G) and IsSphericallyTransitive(G) then
    Info(InfoAutomGrp, 3, "Size(G): infinity, G is spherically transitive");
    return infinity;
  fi;

  if IsFractalByWords(G) then
    Info(InfoAutomGrp, 3, "Size(G): infinity, G is fractal by words");
    return infinity;
  fi;

  if HasIsFractal(G) and IsFractal(G) then
    Info(InfoAutomGrp, 3, "Size(G): infinity, G is fractal");
    return infinity;
  fi;

  if IsSelfSimilarGroup(G) and LevelOfFaithfulAction(G, 8)<>fail then
    return Size(G);
  fi;

  f := FindElementOfInfiniteOrder(G, 10, 10);

  if HasSize(G) or f <> fail then
    return Size(G);
  fi;

  Info(InfoAutomGrp, 1, "You can try to use IsomorphismPermGroup(<G>) or\n",
                        "   FindElementOfInfiniteOrder( <G>, <length>, <depth> ) with bigger bounds");
  TryNextMethod();
end);

InstallOtherMethod(LevelOfFaithfulAction, "for [IsSelfSimGroup and IsSelfSimilar, IsCyclotomic]",
              [IsSelfSimGroup and IsSelfSimilar, IsCyclotomic],
function(G, max_lev)
  local s, s_next, lev;
  if HasIsFinite(G) and not IsFinite(G) then return fail; fi;
  if HasLevelOfFaithfulAction(G) then return LevelOfFaithfulAction(G); fi;
  lev := 0; s := 1; s_next := Size(PermGroupOnLevel(G, 1));
  while s<s_next and lev<max_lev do
    lev := lev+1;
    s := s_next;
    s_next := Size(PermGroupOnLevel(G, lev+1));
  od;
  if s=s_next then
    SetSize(G, s);
    SetLevelOfFaithfulAction(G, lev);
    return lev;
  else
    return fail;
  fi;
end);

InstallMethod(LevelOfFaithfulAction, "for [IsSelfSimGroup and IsSelfSimilar]",
              [IsSelfSimGroup and IsSelfSimilar],
function(G)
  return LevelOfFaithfulAction(G, infinity);
end);

################################################################################
##
#O  IsomorphismPermGroup (<G>)
#O  IsomorphismPermGroup (<G>, <max_lev>)
##
##  For a given finite group <G> generated by initial automata or by elements defined by
##  wreath recursion
##  computes an isomorphism from <G> into a finite permutational group.
##  If <G> is not known to be self-similar (see "IsSelfSimilar") the isomorphism is based on the
##  regular representation, which works generally much slower. If <G> is self-similar
##  there is a level of the tree (see "LevelOfFaithfulAction"), where <G> acts faithfully.
##  The corresponding representation is returned in this case. If <max_lev> is given
##  it finds only the first <max_lev> quotients by stabilizers and if all of them have
##  different size it returns `fail'.
##  If <G> is infinite and <max_lev> is not specified it will loop forever.
##
##  For example, consider a subgroup $\langle a, b\rangle$ of Grigorchuk group.
##  \beginexample
##  gap> Grigorchuk_Group := AutomatonGroup("a=(1,1)(1,2),b=(a,c),c=(a,d),d=(1,b)");
##  < a, b, c, d >
##  gap> f := IsomorphismPermGroup(Group(a, b));
##  MappingByFunction( < a, b >, Group(
##  [ (1,2)(3,5)(4,6)(7,9)(8,10)(11,13)(12,14)(15,17)(16,18)(19,21)(20,22)(23,
##      25)(24,26)(27,29)(28,30)(31,32), (1,3)(2,4)(5,7)(6,8)(9,11)(10,12)(13,
##      15)(14,16)(17,19)(18,20)(21,23)(22,24)(25,27)(26,28)(29,31)(30,32)
##   ]), function( g ) ... end, function( b ) ... end )
##  gap> Size(Image(f));
##  32
##  gap> H := SelfSimilarGroup("a=(a*b,1)(1,2), b=(1,b*a^-1)(1,2), c=(b, a*b)");
##  < a, b, c >
##  gap> f1 := IsomorphismPermGroup(H);
##  MappingByFunction( < a, b, c >, Group([ (1,3)(2,4), (1,3)(2,4), (1,2)
##   ]), function( g ) ... end, function( b ) ... end )
##  gap> Size(Image(f1));
##  8
##  gap> PreImagesRepresentative(f1, (1,3,2,4));
##  a*c
##  gap> (a*c)^f1;
##  (1,3,2,4)
##  \endexample
##
InstallOtherMethod(IsomorphismPermGroup, "for [IsSelfSimilarGroup, IsCyclotomic]",
                   [IsSelfSimGroup and IsSelfSimilar, IsCyclotomic],
function (G, n)
  local H, lev;
  lev := LevelOfFaithfulAction(G, n);
  if lev <> fail then
    H := PermGroupOnLevel(G, LevelOfFaithfulAction(G));
    return AG_GroupHomomorphismByImagesNC(G, H, GeneratorsOfGroup(G), GeneratorsOfGroup(H));
  fi;
  return fail;
end);

###############################################################################
##
#M  IsSphericallyTransitive(G)
##
InstallMethod(IsSphericallyTransitive, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function (G)
  local x, rat_gens, abel_hom, lev;

  if IsFractalByWords(G) then
    Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): true");
    Info(InfoAutomGrp, 3, "  G is fractal");
    return true;
  fi;

  if IsTrivial(G) then
    Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): false");
    Info(InfoAutomGrp, 3, "  G is trivial: G = ", G);
    return false;
  fi;

  if HasIsFinite(G) and IsFinite(G) then
    Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): false");
    Info(InfoAutomGrp, 3, "  IsFinite(G): G = ", G);
    return false;
  fi;

  if DegreeOfTree(G) = 2 and TestSelfSimilarity(G) and IsSelfSimilar(G) then
    if HasIsFinite(G) and IsFinite(G)=false then
      Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): true");
      Info(InfoAutomGrp, 3, "  <G> is infinite self-similar acting on binary tree");
      return true;
    fi;
    if PermGroupOnLevel(G, 2)=Group((1, 4, 2, 3)) then
      Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): true");
      Info(InfoAutomGrp, 3, "  any element which acts transitively on the first level acts spherically transitively");
      return true;
    fi;
  fi;

  for lev in [1..8] do
    if not IsTransitiveOnLevel(G,lev) then
      Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): false");
      Info(InfoAutomGrp, 3, "  the group does not act transitively on level ", lev);
      return false;
    fi;
  od;

  TryNextMethod();
end);

###############################################################################
##
#M  DiagonalPower(<G>, <n>)
##
InstallOtherMethod( DiagonalPower,
                    "for [IsSelfSimGroup and IsGroupOfSelfSimFamily, IsPosInt]",
                    [IsSelfSimGroup and IsGroupOfSelfSimFamily, IsPosInt],
function(G, n)
  return DiagonalPower(UnderlyingSelfSimFamily(G), n);
end);

###############################################################################
##
#M  MultAutomAlphabet(<G>, <n>)
##
InstallOtherMethod( MultAutomAlphabet,
                    "for [IsSelfSimGroup and IsGroupOfSelfSimFamily, IsPosInt]",
                    [IsSelfSimGroup and IsGroupOfSelfSimFamily, IsPosInt],
function(G, n)
  return MultAutomAlphabet(UnderlyingSelfSimFamily(G), n);
end);

###############################################################################
##
#M  \= (<G>, <H>)
##
InstallMethod(\=, "for [IsSelfSimGroup, IsSelfSimGroup]",
              IsIdenticalObj, [IsSelfSimGroup, IsSelfSimGroup],
function(G, H)
  local fgens1, fgens2, fam;

  if HasIsGroupOfSelfSimFamily(G) and HasIsGroupOfSelfSimFamily(H) then
    if IsGroupOfSelfSimFamily(G) <> IsGroupOfSelfSimFamily(H) then
      Info(InfoAutomGrp, 3, "G = H: false, exactly one is GroupOfSelfSimFamily");
      return false;
    fi;
    if IsGroupOfSelfSimFamily(G) then
      Info(InfoAutomGrp, 3, "G = H: true, both are GroupOfSelfSimFamily");
      return true;
    fi;
  fi;

  fgens1 := List(GeneratorsOfGroup(G), g -> Word(g));
  fgens2 := List(GeneratorsOfGroup(H), g -> Word(g));
  fam := UnderlyingSelfSimFamily(G);

  if fam!.rws <> fail then
    fgens1 := AG_ReducedForm(fam!.rws, fgens1);
    fgens2 := AG_ReducedForm(fam!.rws, fgens2);
  fi;

  if GroupWithGenerators(fgens1) = GroupWithGenerators(fgens2) then
    Info(InfoAutomGrp, 3, "G = H: true, by subgroups of free group");
    return true;
  fi;

  TryNextMethod();
end);

###############################################################################
##
#M  IsSubset (<G>, <H>)
##
InstallMethod(IsSubset, "for [IsSelfSimGroup, IsSelfSimGroup]",
              IsIdenticalObj, [IsSelfSimGroup, IsSelfSimGroup],
function(G, H)
  local h, fam, fgens1, fgens2;

  if HasIsGroupOfSelfSimFamily(G) and IsGroupOfSelfSimFamily(G) then
    Info(InfoAutomGrp, 3, "IsSubgroup(G, H): true");
    Info(InfoAutomGrp, 3, "  G is GroupOfSelfSimFamily");
    return true;
  fi;

  fgens1 := List(GeneratorsOfGroup(G), g -> Word(g));
  fgens2 := List(GeneratorsOfGroup(H), g -> Word(g));
  fam := UnderlyingSelfSimFamily(G);

  if fam!.rws <> fail then
    fgens1 := AG_ReducedForm(fam!.rws, fgens1);
    fgens2 := AG_ReducedForm(fam!.rws, fgens2);
  fi;

  if IsSubgroup(GroupWithGenerators(fgens1), GroupWithGenerators(fgens2)) then
    Info(InfoAutomGrp, 3, "IsSubgroup(G, H): true");
    Info(InfoAutomGrp, 3, "  by subgroups of free group");
    return true;
  fi;

  TryNextMethod();
end);

###############################################################################
##
#M  <g> in <G>
##
InstallMethod(\in, "for [IsSelfSim, IsSelfSimGroup]",
              [IsSelfSim, IsSelfSimGroup],
function(g, G)
  local fam, fgens, w;

  if HasIsGroupOfSelfSimFamily(G) and IsGroupOfSelfSimFamily(G) then
    return true;
  fi;

  fgens := List(GeneratorsOfGroup(G), g -> Word(g));
  w := Word(g);

  fam := UnderlyingSelfSimFamily(G);

  if fam!.rws <> fail then
    fgens := AG_ReducedForm(fam!.rws, fgens);
    w := AG_ReducedForm(fam!.rws, w);
  fi;

  if w in GroupWithGenerators(fgens) then
    Info(InfoAutomGrp, 3, "g in G: true");
    Info(InfoAutomGrp, 3, "  by elements of free group");
    Info(InfoAutomGrp, 3, "  g = ", g, "; G = ", G);
    return true;
  fi;

  TryNextMethod();
end);

###############################################################################
##
#O  Random(<G>)
##
##  Returns a random element of a group (semigroup) <G>. The operation is based
##  on the generator of random elements in free groups and semigroups.
##
##  \beginexample
##  gap> Basilica := AutomatonGroup( "u=(v,1)(1,2), v=(u,1)" );
##  < u, v >
##  gap> Random( Basilica );
##  v*u^-3
##  \endexample
##
InstallMethodWithRandomSource(Random, "for a random source and [IsSelfSimGroup]",
              [IsRandomSource, IsSelfSimGroup],
function(rs, G)
  local F, gens, pi;

  if IsTrivial(G) then
    return One(G);
  elif IsSelfSimilarGroup(G) then
    return SelfSim(Random(rs, UnderlyingFreeGroup(G)), UnderlyingSelfSimFamily(G));
  else
    gens := GeneratorsOfGroup(G);
    F := FreeGroup(Length(gens));
    pi := GroupHomomorphismByImagesNC(F, G,  GeneratorsOfGroup(F), gens);
    return Random(rs, F)^pi;
  fi;
end);

###############################################################################
##
#M  UnderlyingFreeSubgroup(<G>)
##
InstallMethod(UnderlyingFreeSubgroup, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  local f;
  if HasIsGroupOfSelfSimFamily(G) and IsGroupOfSelfSimFamily(G) then
    return UnderlyingFreeGroup(G);
  fi;
  f := Subgroup(UnderlyingFreeGroup(G), UnderlyingFreeGenerators(G));
  if f = UnderlyingFreeGroup(G) then
    SetIsGroupOfSelfSimFamily(G, true);
  fi;
  return f;
end);

###############################################################################
##
#M  UnderlyingFreeGenerators(<G>)
##
InstallMethod(UnderlyingFreeGenerators, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  return List(GeneratorsOfGroup(G), g -> Word(g));
end);

###############################################################################
##
#M  TrivialSubmagmaWithOne(<G>)
##
InstallMethod(TrivialSubmagmaWithOne, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  return Subgroup(G, [One(G)]);
end);

###############################################################################
##
#M  IsSelfSimilarGroup(<G>)
##
##  Returns `true' if generators of <G> coincide with generators of the family
InstallImmediateMethod(IsSelfSimilarGroup, IsSelfSimGroup, 0,
function(G)
  local fam;
  fam := UnderlyingSelfSimFamily(G);
  return fam!.numstates = 0 or
         GeneratorsOfGroup(G) = fam!.recurgens{[1..fam!.numstates]};
end);

###############################################################################
##
#M  RecurList(<G>)
##
InstallMethod(RecurList, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  if IsSelfSimilarGroup(G) then
    return RecurList(GroupOfSelfSimFamily(UnderlyingSelfSimFamily(G)));
  else
    Error("Group <G> is not necessarily self-similar");
  fi;
end);

###############################################################################
##
#M  IsSelfSimilar(<G>)
##
InstallMethod(IsSelfSimilar, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  local g, i, res;
  res := true;
  for g in GeneratorsOfGroup(G) do
    for i in [1..UnderlyingSelfSimFamily(G)!.deg] do
      res := Section(g, i) in G;
      if res = fail then
        TryNextMethod();
      elif not res then
        return false;
      fi;
    od;
  od;
  return true;
end);

###############################################################################
##
#M  UnderlyingSelfSimFamily(<G>)
##
InstallMethod(UnderlyingSelfSimFamily, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  return FamilyObj(GeneratorsOfGroup(G)[1]);
end);

###############################################################################
##
#M  IsFiniteState(<G>)
##
InstallMethod(IsFiniteState, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  local states, MealyAutomatonLocal, aut_list, gens, images, H, g, hom_function, \
        inv_hom_function, hom, free_groups_hom, inv_free_groups_hom, inv_hom, \
        gens_in_freegrp, images_in_freegrp, preimages_in_freegrp, F, pi, pi_bar, \
        preimage_in_freegrp, MealyAutomatonLocalFinite;

# if we do not know much, we compare just words in free group
  MealyAutomatonLocal := function(g)
    local cur_state;
    if g!.word in states then return Position(states, g!.word); fi;
    Add(states, g!.word);
    cur_state := Length(states);
    aut_list[cur_state] := List([1..g!.deg], x -> MealyAutomatonLocal(Section(g, x)));
    Add(aut_list[cur_state], g!.perm);
    return cur_state;
  end;

# if we do know that the groups is finite, we compare actual elements of the group
  MealyAutomatonLocalFinite := function(g)
    local cur_state;
    if g in states then return Position(states, g); fi;
    Add(states, g);
    cur_state := Length(states);
    aut_list[cur_state] := List([1..g!.deg], x -> MealyAutomatonLocalFinite(Section(g, x)));
    Add(aut_list[cur_state], g!.perm);
    return cur_state;
  end;

  if IsTrivial(G) then return true; fi;

  states := [];
  aut_list := [];
  gens := GeneratorsOfGroup(G);
  images := [];

  if HasIsFinite(G) and IsFinite(G) then
    for g in gens do
      Add(images, MealyAutomatonLocalFinite(g));
    od;
    states := List(states, Word);
  else
    for g in gens do
      Add(images, MealyAutomatonLocal(g));
    od;
  fi;

  H := AutomatonGroup(aut_list);

  if IsTrivial(H) then
    SetIsTrivial( G, true);
    return true;
  fi;

  images := UnderlyingAutomFamily(H)!.oldstates{images};

  SetIsomorphicAutomGroup(G, GroupWithGenerators(UnderlyingAutomFamily(H)!.automgens{images}));
  SetUnderlyingAutomatonGroup(G, H);

# preimages of generators of G in UnderlyingFreeGroup(G)
  gens_in_freegrp := List(GeneratorsOfGroup(G), Word);

# preimages of generators of a subgroup of H isomorphic to G in UnderlyingFreeGroup(H)
  images_in_freegrp := List(UnderlyingAutomFamily(H)!.automgens{images}, Word);

  preimage_in_freegrp := function(x)
    local w;
    w := LetterRepAssocWord(x!.word)[1];
    if w > 0 then
      return states[ Position( UnderlyingAutomFamily(H)!.oldstates, w)];
    else
      return states[ Position( UnderlyingAutomFamily(H)!.oldstates, -w+UnderlyingAutomFamily(H)!.numstates)];
    fi;
  end;

#  preimages of generators of H in UnderlyingFreeGroup(G)
#  preimages_in_freegrp := List([1..Length(GeneratorsOfGroup(H))], x->states[Position(UnderlyingAutomFamily(H)!.oldstates, x)]);
  preimages_in_freegrp := List(GeneratorsOfGroup(H), x -> preimage_in_freegrp(x));

  if IsSelfSimilarGroup(G) then
    free_groups_hom :=
       GroupHomomorphismByImagesNC( Group(gens_in_freegrp), UnderlyingFreeGroup(H),
                                    gens_in_freegrp, images_in_freegrp );

    inv_free_groups_hom :=
       GroupHomomorphismByImagesNC( UnderlyingFreeGroup(H), UnderlyingFreeGroup(G),
                                    UnderlyingFreeGenerators(H), preimages_in_freegrp );

    hom_function := function(a)
      return Autom(Image(free_groups_hom, a!.word), UnderlyingAutomFamily(H));
    end;

    inv_hom_function :=  function(b)
      return SelfSim(Image(inv_free_groups_hom, b!.word), UnderlyingSelfSimFamily(G));
    end;

    hom := GroupHomomorphismByFunction(G, GroupWithGenerators(UnderlyingAutomFamily(H)!.automgens{images}), hom_function, inv_hom_function);

    SetMonomorphismToAutomatonGroup(G, hom);
  else
    F := FreeGroup(Length(GeneratorsOfGroup(G)));

#        pi
#    F ------> G ----> UnderlyingFreeGroup(H)
#      -------------->
#            pi_bar

    pi := GroupHomomorphismByImages(F,                     Group(gens_in_freegrp),
                                    GeneratorsOfGroup(F),  gens_in_freegrp);

    pi_bar := GroupHomomorphismByImages(F,                     UnderlyingFreeGroup(H),
                                        GeneratorsOfGroup(F),  images_in_freegrp);

    hom_function := function(g)
      return Autom(Image(pi_bar, PreImagesRepresentative(pi, g!.word)), UnderlyingAutomFamily(H));
    end;

    inv_hom_function :=  function(b)
      return SelfSim(Image(pi, PreImagesRepresentative(pi_bar, b!.word)), UnderlyingSelfSimFamily(G));
    end;

    hom := GroupHomomorphismByFunction(G, GroupWithGenerators(UnderlyingAutomFamily(H)!.automgens{images}), hom_function, inv_hom_function);

    SetMonomorphismToAutomatonGroup(G, hom);
  fi;

  return true;
end);

###############################################################################
##
#M  IsomorphicAutomGroup( <G> )
##
InstallMethod(IsomorphicAutomGroup, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  if IsFiniteState(G) then return IsomorphicAutomGroup(G); fi;
end);

###############################################################################
##
#M  UnderlyingAutomatonGroup( <G> )
##
InstallMethod(UnderlyingAutomatonGroup, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  if IsFiniteState(G) then return UnderlyingAutomatonGroup(G); fi;
end);

###############################################################################
##
#M  MonomorphismToAutomatonGroup( <G> )
##
InstallMethod(MonomorphismToAutomatonGroup, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  if IsFiniteState(G) then return MonomorphismToAutomatonGroup(G); fi;
end);

###############################################################################
##
#M  IsContracting( <G> )
##
InstallMethod(IsContracting, "for [IsSelfSimilarGroup]",
              [IsSelfSimilarGroup],
function(G)
  local res;
  if not IsFiniteState(G) then
#   every contracting self-similar group is finite-state
    return false;
  fi;

  res := IsContracting(GroupOfAutomFamily(UnderlyingAutomFamily(UnderlyingAutomatonGroup(G))));

  UnderlyingSelfSimFamily(G)!.use_contraction := true;
  UnderlyingAutomFamily(UnderlyingAutomatonGroup(G))!.use_contraction := true;

  return res;
end);

###############################################################################
##
#M  GroupNucleus( <G> )
##
InstallMethod(GroupNucleus, "for [IsSelfSimilarGroup]",
              [IsSelfSimilarGroup],
function(G)
  local H;
  if not IsFiniteState(G) then
#   every contracting self-similar group is finite-state
    Error("Group <G> is not finite-state");
  fi;

  if not IsContracting(G) then
    Error("Group <G> is not contracting");
  fi;

  H := GroupOfAutomFamily( UnderlyingAutomFamily( UnderlyingAutomatonGroup(G)));

  return List( GroupNucleus(H), x -> PreImagesRepresentative( MonomorphismToAutomatonGroup(G), x));
end);

###############################################################################
##
#M  UseContraction( <G> )
##
InstallMethod(UseContraction, "for [IsSelfSimGroup]", true,
              [IsSelfSimGroup],
function(G)
  if not IsSelfSimilarGroup(G) then
    Print("Error in UseContraction(<G>): The method is implemented only for IsSelfSimilarGroup\n");
    return fail;
  fi;

  if not HasIsContracting(G) then
    Print("Error in UseContraction(<G>): It is not known whether the group <G> is contracting\n");
    return fail;
  elif not IsContracting(G) then
    Print("Error in UseContraction(<G>): The group <G> is not contracting");
    return fail;
  fi;
  #  IsContracting returns either true or false or an error (it can not return fail)

  UnderlyingSelfSimFamily(G)!.use_contraction := true;
  UnderlyingAutomFamily( UnderlyingAutomatonGroup(G))!.use_contraction := true;

  return true;
end);

###############################################################################
##
#M  DoNotUseContraction( <G> )
##
InstallMethod(DoNotUseContraction, "for [IsSelfSimGroup]", true,
              [IsSelfSimGroup],
function(G)
  UnderlyingAutomFamily(G)!.use_contraction := false;

  if HasUnderlyingAutomatonGroup(G) then
    UnderlyingAutomFamily( UnderlyingAutomatonGroup(G))!.use_contraction := false;
  fi;
  return true;
end);

###############################################################################
##
#M  FindNucleus( <G> )
##
InstallMethod(FindNucleus, "for [IsSelfSimilarGroup, IsCyclotomic]",
              [IsSelfSimilarGroup, IsCyclotomic],
function(G, max_nucl)
  local H, nuclH, nuclG;
  if not IsFiniteState(G) then
#   every contracting self-similar group is finite-state
    Error("Group <G> is not finite-state");
  fi;

  if HasIsContracting(G) and not IsContracting(G) then
    Error("Group <G> is not contracting");
  fi;

  H := GroupOfAutomFamily( UnderlyingAutomFamily( UnderlyingAutomatonGroup( G )));

  if HasIsContracting(H) and not IsContracting(H) then
    Error("Group <G> is not contracting");
  fi;

  nuclH := FindNucleus(H, max_nucl);

  if nuclH=fail then return fail; fi;

  nuclG := [];
  Add(nuclG, List( GeneratingSetWithNucleus(H), x -> PreImagesRepresentative( MonomorphismToAutomatonGroup( G ), x )));
  Add(nuclG, List( GroupNucleus(H), x -> PreImagesRepresentative( MonomorphismToAutomatonGroup( G ), x )));
  Add(nuclG, GeneratingSetWithNucleusAutom(H));

  SetGroupNucleus(G, nuclG[1]);
  SetGeneratingSetWithNucleus(G, nuclG[2]);
  SetGeneratingSetWithNucleusAutom(G, nuclG[3]);
  SetContractingLevel(G, ContractingLevel(H));

  return nuclG;
end);

InstallMethod(FindNucleus, "for [IsSelfSimilarGroup]", true,
              [IsSelfSimilarGroup],
function(G)
  return FindNucleus(G, infinity);
end);

InstallMethod(GeneratingSetWithNucleus, "for [IsSelfSimilarGroup]", true,
              [IsSelfSimilarGroup],
function(G)
  if IsContracting(G) then return GeneratingSetWithNucleus(G); fi;
end);

InstallMethod(GeneratingSetWithNucleusAutom, "for [IsSelfSimilarGroup]", true,
              [IsSelfSimilarGroup],
function(G)
  if IsContracting(G) then return GeneratingSetWithNucleusAutom(G); fi;
end);

#E


EW schwarz,1,Projekt		//same as in cube.gap
  S(2,2,3)			//redifine edge size as (x,y,z)


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

Class(Codegen, HierarchicalVisitor, rec(
    initXY := function(x, y, opts)
        if IsBound(opts.XType) and not IsList(x) then
            if not IsArrayT(opts.XType) and not IsPtrT(opts.XType)
                then Error("opts.XType must be a pointer or array type. This has recently changed.",
                           "If you used TReal before, use TPtr(TReal) now"); fi;
            x.t := opts.XType;
            x.t := When(IsBound(opts.useRestrict) and opts.useRestrict, x.t.restrict(), x.t);
        fi;
        if IsBound(opts.YType) and not IsList(y) then
            if not IsArrayT(opts.YType) and not IsPtrT(opts.YType)
                then Error("opts.YType must be a pointer or array type. This has recently changed.",
                           "If you used TReal before, use TPtr(TReal) now"); fi;
            y.t := opts.YType;
            y.t := When(IsBound(opts.useRestrict) and opts.useRestrict, y.t.restrict(), y.t);
        fi;
        return [x, y];
    end,

    # NOTE: get rid of _acc_

    # This function substitutes assign by assign_acc (eliminated in a subsequent pass)
    # This is done to achieve accumulation, and assign_acc is used to
    #   prevent double accumulation.. Used in Codegen.ISumAcc and Codegen.SUMAcc.
    # Must be handled better somehow.
    #
    _acc := (icode, y) ->
        SubstTopDownNR(icode, [assign, [@(0,nth), @(1), @(2)], @(3)],
            e -> assign_acc(@(0).val, @(3).val)),

    _interleave := function(codes)
        local decls, c, i, j, cmds;
        decls := Set([]);
        for i in [1..Length(codes)] do
            c := codes[i];
            while ObjId(c)=decl do UniteSet(decls, c.vars); c := c.cmd; od;
            if ObjId(c)<>chain then
                codes[i] := [c];
            else
                codes[i] := c.cmds;
            fi;
        od;

        cmds := [];
        for i in [1..Maximum(List(codes, Length))] do
            for j in [1..Length(codes)] do
                if IsBound(codes[j][i]) then Add(cmds, codes[j][i]); fi;
            od;
        od;
        return decl(decls, chain(cmds));
    end
));

#fAdd.rlambda := self >> let(i:=Ind(), Lambda(i,i+1));
#H.rlambda := self >> let(i:=Ind(), Lambda(i,i+self.params[4]));

# a version of the Dat1d call that also sets the 'no scalarize' flag on the variable 't'.
_dat1d_donotscalarize := function(a,b)
    local t;

    t := Dat1d(a,b);
    t.doNotScalarize := true;

    return t;
end;

Class(DefaultCodegen, Codegen, rec(
    Formula := meth(self, o, y, x, opts)
        local icode, datas, prog, params, sub, initsub, destroysub, io, t, initcode, initparams;

        o := SumsUnification(o.child(1), opts);

        [x, y] := self.initXY(x, y, opts);

        #o :=  Process_fPrecompute(o, opts);

        params := Set(Concatenation(Collect(o, param), Filtered(Collect(o, var), IsParallelLoopIndex)));

        datas := Collect(o, FDataOfs);
        [o,t] := UTimedAction(BlockSumsOpts(o, opts)); #PrintLine("BlockSums ", t);
        [icode,t] := UTimedAction(self(o, y, x, opts)); #PrintLine("codegen ", t);
        #[icode,t] := UTimedAction(ESReduce(icode, opts)); #PrintLine("ESReduce ", t);
        icode := RemoveAssignAcc(icode);
        Unbind(Compile.times);
        [icode,t] := UTimedAction(BlockUnroll(icode, opts)); #PrintLine("BlockUnroll ", t);
        #PrintLine("---compile--");
        #DoForAll([1..Length(Compile.times)], i -> PrintLine(i, " ", Compile.times[i], " ",
        #        let(f:=opts.compileStrategy[i], When(IsFunc(f) or IsMeth(f), "---", f))));

        # icode := PowerOpt(icode);
        icode := DeclareHidden(icode);
        if IsBound(opts.isFixedPoint) and opts.isFixedPoint then
            icode := FixedPointCode(icode, opts.bits, opts.fracbits);
        fi;

        initparams := Copy(params);
        if IsBound(opts.symbol) then
          params := Concatenation(params, opts.symbol);
        fi;

        if IsBound(opts.accStrategy) then
          icode.iy := y;
          icode.iy.n := o.dims()[1];
          icode.ix := x;
          icode.ix.n := o.dims()[2];
          icode.ivars := Concatenation(params, List(datas, x->x.var));
          icode := opts.accStrategy(icode);
        fi;

        io := When(x=y, [x], [y, x]);
        sub := Cond(IsBound(opts.subName), opts.subName, "transform");
        initsub := Cond(IsBound(opts.subName), Concat("init_", opts.subName), "init");
        destroysub := Cond(IsBound(opts.subName), Concat("destroy_", opts.subName), "destroy");
        icode := func(TVoid, sub, Concatenation(io, params), icode);

        if IsBound(opts.generateInitFunc) and opts.generateInitFunc then
	    initcode := chain(List(Filtered(datas, e -> IsBound(e.var.init)), x -> SReduce(x.var.init, opts)));
            prog := program(
                decl(List(datas, x->x.var),
                    chain(
                        func(TVoid, initsub, initparams :: Set(Collect(initcode, param)), initcode),
                        icode,
                        func(TVoid, destroysub, [], skip())
                    )));
        else
            prog := program( func(TVoid, initsub, params, chain()), icode);
        fi;
        prog.dimensions := o.dims();
        return prog;
    end,

    Cross := meth(self, o, y, x, opts)
        local i, mychain, xdims, ydims, myYdims, myXdims;
        mychain:=[];xdims:=1;ydims:=1;
        for i in [1..Length(o._children)] do
            myYdims:=DimLength(o._children[i].dims()[1]);
            myXdims:=DimLength(o._children[i].dims()[2]);
            Add(mychain,self(o._children[i],
                    StripList(y{[ydims..ydims+myYdims-1]}),
                    StripList(x{[xdims..xdims+myXdims-1]}),opts));
            ydims:=ydims+myYdims;
            xdims:=xdims+myXdims;
        od;
        return chain(mychain);
    end,

    Glue:= meth(self,o,y,x,opts)
        local size, als,iterator,n;
        size := o.element[2];
        n := EvalScalar(o.element[1]);
        iterator :=Ind(size);
        als  := List([0..n-1],t -> assign(nth(StripList(y),add(iterator,t*size)),nth(x[t+1],iterator)));
        return loop(iterator, size, chain(als));
    end,

    Split:= meth(self,o,y,x,opts)
        local size, als,iterator,n;
        n := EvalScalar(o.element[2]);
        size := o.element[1];
        iterator :=Ind(idiv(size, n));
        als  := List([0..(n-1)],t -> assign(nth(y[t+1],iterator),nth(StripList(x),add(iterator,t*size/n))));
        return loop(iterator, idiv(size, n), chain(als));
     end,

    BB := (self,o,y,x,opts) >> MarkForUnrolling(
        When(IsBound(o.bbnum),
            o.bbnum,
            0
        ),
        self(o.child(1), y, x, opts),
        opts
    ),
    IDirSum := (self,o,y,x,opts) >> self(o.sums(), y, x, opts),
    TTag := (self,o,y,x,opts) >> self(o.params[1], y, x, opts),
    DPWrapper := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),

    Buf := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    NoPull := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    PushL := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    PushR := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    PushLR := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    Grp := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    NoDiagPullin := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    NoDiagPullinLeft := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    NoDiagPullinRight := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),

    COND := (self,o,y,x,opts) >> IF(
        When(IsFunction(o.cond), o.cond.at(0), o.cond),
        self(o.child(1), y, x, opts), self(o.child(2), y, x, opts)),

    Diag := (self, o, y, x, opts) >> let(i := Ind(), elt := o.element.lambda(),
        loop(i, elt.domain(), assign(nth(y,i), elt.at(i) * nth(x,i)))),

    DiagCpxSplit := (self, o, y, x, opts) >> let(i := Ind(), elt := o.element.lambda(),
        re := elt.at(2*i), im := elt.at(2*i+1),
        loop(i, elt.domain()/2,
             assign(nth(y,i),   re * nth(x,i) + E(4) * im * nth(x,i)))),

    TCast := (self, o, y, x, opts) >> let(i := Ind(),
        loop(i, o.params[1], assign(nth(y,i), tcast(o.params[2], nth(x,i))))),

    RCDiag := (self, o, y, x, opts) >> let(i := Ind(), elt := o.element.lambda(),
        re := elt.at(2*i), im := elt.at(2*i+1),
        loop(i, elt.domain()/2, chain(
             assign(nth(y,2*i),   re * nth(x,2*i) - im * nth(x,2*i+1)),
             assign(nth(y,2*i+1), im * nth(x,2*i) + re * nth(x,2*i+1))))),

    ColVec := (self, o, y, x, opts) >> let(i := Ind(), func := o.element.lambda(),
        loop(i, func.domain(), assign(nth(y,i), mul(func.at(i), nth(x,0))))),

    RowVec := (self, o, y, x, opts) >> let(i := Ind(), func := o.element.lambda(),
        t := TempVar(x.t.t),
        chain(assign(t,0),
            loop(i, func.domain(), assign(t, add(t, mul(func.at(i), nth(x,i))))),
            assign(nth(y,0), t))),

    Scale := (self, o, y, x, opts) >> let(i := Ind(),
        chain(self(o.child(1), y, x, opts),
              loop(i, Rows(o), assign(nth(y,i), mul(o.scalar, nth(y,i)))))),

    I := (self, o, y, x, opts) >> Cond(x<>y,let(i := Ind(Rows(o)),
            loop(i, i.range, assign(nth(y,i), nth(x,i)))),skip()),

    2DI := (self, o, y, x, opts) >> Cond(x<>y, Error("Should not happen"), skip()),

    # this one will always unroll Blk's
    Blk := (self, o, y, x, opts) >> let(
	# this is a hack, and it is needed, because without it we will be assigning to a
	# variable of type TArray (after binsplit), and some compilers (=icc) don't like that
	# (not an l-value)
	# NOTE: suggestion -- move this somewhere, as a compatibility patch (maybe postprocess?)
	tcst := Cond(IsSymbolic(o.element), x->tcast(TPtr(o.element.t.t.t), x), x->x),
	Cond(
            not IsSymbolic(o.element) and Rows(o)=2 and Cols(o)=2, Blk2code(o, y, x),
            not IsSymbolic(o.element) and Rows(o)=4 and Cols(o)=4, Blk4code(o, y, x),
            chain(
		List([0..Rows(o)-1], j -> let(
		    row := tcst(nth(o.element, j)),
                    assign(nth(y,j), ApplyFunc(add, List([0..Cols(o)-1], i -> nth(row, i) * nth(x,i))))
		))
	    )
	)
    ),

# This one can loop Blk's if needed, but is much slower for large matrices, and eventually blows up in storage requirements
#    Blk := (self, o, y, x, opts) >> Cond(
#        Rows(o)=2 and Cols(o)=2, Blk2code(o, y, x),
#        Rows(o)=4 and Cols(o)=4, Blk4code(o, y, x),
#        let(j:=Ind(), i:=Ind(), t:=TempVar(x.t.t), mat:=V(o.element), d:=Dat(mat.t),
#            data(d, mat,
#                loop(j, Rows(o), decl(t, chain(
#                     assign(t, 0),
#                     loop(i, Cols(o), assign(t, t + nth(nth(d,j),i) * nth(x,i))),
#                     assign(nth(y,j), t))))))),

    toeplitz := (self, o, y, x, opts) >> self.Blk(o.obj, y, x, opts),

    Blk1 := (self, o, y, x, opts) >> assign(nth(y,0), mul(toExpArg(o.element), nth(x,0))),

    BlkConj := (self, o, y, x, opts) >> assign(nth(y,0), conj(nth(x,0))),

    Prm := (self, o, y, x, opts) >> Cond(
        x = y,
            When(ObjId(o.func) = fId, skip(), Error("Inplace Permutation is not dealt with...")),

        let(i:=Ind(), func:=o.func.lambda(),
            loop(i, Rows(o), assign(nth(y, i), nth(x, func.at(i)))))
    ),

    O := (self, o, y, x, opts) >> let(i:=Ind(),
        loop(i, o.params[1], assign(nth(y, i), V(0)))),

    Gath := meth(self, o, y, x, opts)
        local i, func, rfunc, ix;
        i := Ind(); func := o.func.lambda();

        if IsBound(o.func.rlambda) then
            rfunc := o.func.rlambda();
            ix := var.fresh_t("ix", TInt);
            return decl(ix, chain(
                    assign(ix, func.at(0)),
                    assign(nth(y, 0), nth(x, ix)),
                    loop(i, o.func.domain()-1,
                        chain(assign(ix, rfunc.at(ix)),
                            assign(nth(y, i+1), nth(x, ix))))));
        else
            return loop(i, o.func.domain(), assign(nth(y,i), nth(x, func.at(i))));
        fi;
    end,

    Scat := meth(self, o, y, x, opts)
        local i, func, rfunc, ix;
        i := Ind(); func := o.func.lambda();
        if IsBound(o.func.rlambda) then
        rfunc := o.func.rlambda();
        ix := var.fresh_t("ix", TInt);
        return decl(ix, chain(
                assign(ix, func.at(0)),
                assign(nth(y, ix), nth(x, 0)),
                loop(i, o.func.domain()-1,
                    chain(assign(ix, rfunc.at(ix)),
                        assign(nth(y, ix), nth(x, i+1))))));
        else
            return loop(i, o.func.domain(), assign(nth(y,func.at(i)), nth(x, i)));
        fi;
    end,

    ScatGath := meth(self, o, y, x, opts)
        local i, sfunc, gfunc, decls;

        i := Ind();
        sfunc := o.sfunc.lambda();
        gfunc := o.gfunc.lambda();
        decls := Set(Concat(sfunc.free(),gfunc.free()));
        return loopn(i, o.sfunc.domain(), assign(nth(y,sfunc.at(i)), nth(x, gfunc.at(i))));
    end,

    SUM := (self, o, y, x, opts) >> chain(List(o.children(), c -> self(c, y, x, opts))),

    ISum := (self, o, y, x, opts) >> let(
        myloop := When(IsSymbolic(o.domain), loopn, loop),
        myloop(o.var, o.domain,
            self(o.child(1), y, x, opts))),

    JamISum := (self, o, y, x, opts) >> let(its := EvalScalar(o.domain),
        Cond(IsSymbolic(its),
                 self.ISum(o, y, x, opts),
             its > 32, Error("<o.domain> is too big (> 32), probably something went wrong. ",
                             "If you know what you are doing, then change DefaultCodegen.JamISum"),
             let(s := o.child(1),
                 bodies := List([0..its-1], j ->
                     Compile(self(SubstVars(Copy(s), rec((o.var.id):= V(j))), y, x, opts), opts)),
                 self._interleave(bodies)))),

    # NOTE: get rid of _acc
    SUMAcc := (self, o, y, x, opts) >> let(ii := Ind(),
        When(not Same(ObjId(o.child(1)), Gath),  # NOTE: come up with a general condition
            chain(
                loop(ii, Rows(o), assign(nth(y, ii), V(0))),
                List(o.children(), c -> self._acc(self(c, y, x, opts), y))),
            chain(
                self(o.child(1), y, x, opts),
                List(Drop(o.children(), 1), c -> self._acc(self(c, y, x, opts), y))))),

    ISumAcc := (self, o, y, x, opts) >> let(ii := Ind(), chain(
            loop(ii, Rows(o), assign(nth(y, ii), V(0))),
            loop(o.var, o.domain, self._acc(self(o.child(1), y, x, opts), y)))),

    ScatAcc := (self, o, y, x, opts) >> self._acc(self(Scat(o.func),y,x,opts),y),

    # _composePropagate(<ch>, <y>, <x>, <mfunc>) - <x> and <y> and temp arrays propagation through
    # identity/inplace operators and creating intermediate arrays using <mfunc> which
    # has (c) -> ... signature (<c> is child operator). There is no assumption made on what is <x>,
    # <y> and <mfunc> return so this function can be used to propagate other information same way
    # as arrays in composition.

    _composePropagate := function(ch, y, x, mfunc)
        local numch, vecs, i, j, cmd, code, isI, crossCh, apos;
        numch := Length(ch);
        vecs  := [y];

        isI   := (x) -> IsIdentitySPL(x) or x.isInplace();

        # we like to evaluate right (last) to left (first), where the values
        # in parens refer to the position of the elements in the array.
        #
        # So here, we start at the output (first entry in array) and
        # walk towards the input, creating temporary arrays as necessary

        for i in [1..numch-1] do
            vecs[i+1] := When(isI(ch[i]),
                vecs[i],
                mfunc(ch[i])
            );
            if ObjId(ch[i]) = Cross then
                apos := [ 1, 1 ]; # apos holds starting input/output indexes
                                  # as children's arity on input may be different from output arity.
                crossCh := ch[i].rChildren();
                for j in [1 .. Length(crossCh)] do
                    if isI(crossCh[j]) then
                        vecs[i+1][apos[2]] := vecs[i][apos[1]];
                    fi;
                    apos := apos + crossCh[j].arity();
                od;
            fi;
        od;

        # the last entry must be the input.
        vecs[numch+1] := x;

        # now we walk in the opposite direction, copying through
        # input as far as we can.
        # if there is a cross and one of the inputs is an identity,
        # there's no point actually doing it
        for i in Reversed([1..numch]) do
            if isI(ch[i]) then
                vecs[i] := vecs[i+1];
            elif ObjId(ch[i]) = Cross then
                apos := [ 1, 1 ]; # apos holds starting input/output indexes
                                  # as children's arity on input may be different from output arity.
                crossCh := ch[i].rChildren();
                for j in [1 .. Length(crossCh)] do
                    if isI(crossCh[j]) then
                        vecs[i][apos[1]] := vecs[i+1][apos[2]];
                    fi;
                    apos := apos + crossCh[j].arity();
                od;
            fi;
        od;

        # If all children were evaluated inplace, the output will be in x
        # Make it go from x -> y as expected
        if vecs[1] = vecs[numch+1] then vecs[1] := y; fi;
        if vecs[1] = vecs[numch+1] then vecs[numch+1] := x; fi;

        return vecs;
    end,

    Compose := meth(self, o,y,x,opts)
        local ch, numch, vecs;
        ch    := o.children();
        numch := Length(ch);

        # propagate x and y arrays and create temporary arrays
        vecs  := self._composePropagate(ch, y, x, c -> TempArray(y,x,c));

        # order them so that first to be evaluated is first in array.
        vecs := Reversed(vecs);
        ch   := Reversed(ch);

        # Wrap code in variable declaration. Each entry in vecs will contain multiple
        # arrays in the case of multi-input/output (i.e. OL)
        return decl(
            Difference(Flat(vecs{[2..Length(vecs)-1]}), Flat([x,y])),
            chain(
                List([1..numch], i ->
                     self(ch[i], vecs[i+1], vecs[i], opts)))
        );
    end,

    # ComposeDists is meant to be like a Compose, but for parallel ISums. We
    # need it for 2 reasons: a) The arrays declared are distributed among the
    # nodes, so we need them to be of size N/p, and not N. b) We can ping-pong
    # between 2 parallel buffers instead of declaring a ton of temp arrays, as
    # long as a barrier-sync exists between the stages. NOTE: This won't work
    # if we do full overlapping with micro-barriers. NOTE: This also might not
    # work too well for partial/dirty-overlap.

    ComposeDists := meth(self, o,y,x,opts)
        local ch, numch, vecs, i, cmd, code, pt1, pt2;
        ch    := o.children();
        numch := Length(ch);

        # order them so that first to be evaluated is first in array.
        ch   := Reversed(ch);

        # NOTE: We assume that the # of procs doesn't change across the ComposeDists!
        pt1 := TempArraySeq(y,x,ch[1]);
        pt2 := TempArraySeq(y,x,ch[1]);

        return(
            decl([pt1, pt2], chain(
            List([1..numch], i ->
                let(ppx := When(i mod 2 = 0, pt1, pt2),
                    ppy := When(i mod 2 = 0, pt2, pt1),
                    px  := When(i=1,     x, ppx),
                    py  := When(i=numch, y, ppy),
                    self(ch[i], py, px, opts))
                )
            ))
        );
    end,

    ComposeStreams:= meth(self, o,y,x,opts)

        # This is a compose of 2 or more multibuffered streams. The streams can
        # ping-pong data between X and Y. This is okay only if we're allowed to
        # clobber the input (reasonable to assume only when Inplace is
        # requested). Also, whether the last stage is a ping or a pong will
        # determine where the output is written to (which is not great).
        # Currently, just a hack.

        local ch, numch;
        ch    := o.children();
        numch := Length(ch);

        # order them so that first to be evaluated is first in array.
        ch   := Reversed(ch);

        return chain( List([1..numch], i ->
            let(ppy := When(i mod 2 = 0, x, y),
                ppx := When(i mod 2 = 0, y, x),
                self(ch[i], ppy, ppx, opts))
            )
        );
    end,

    Inplace := (self, o, y, x, opts) >>
        self(o.child(1), y, x, opts), # Compose will handle these somehow
#MRT: it really should do this... but it doesn't
#        self(o.child(1), x, x, opts),

    Data := meth(self, o, y, x, opts)
        local val;
        o.var.isData := true;
        val := When(IsFunction(o.value), o.value.tolist(), o.value);
        val := When(IsValue(val), val, o.var.t.value(val));
        return data(o.var, val, self(o.child(1), y, x, opts));
    end,

    #   NOTE: use pointers to do pingpong?
    ICompose := (self, o, y, x, opts) >> let(
        t      := Dat1d(x.t.t, Rows(o)),
        its    := o.domain,
        newind := Ind( Int((its-1)/2) ),
        # if orig loops has even # iterations, we peel 2 iterations, otherwise peel 1
        peel_its := Cond(IsEvenInt(o.domain), 2, 1),

        decl([t], chain(
           Cond(IsOddInt(o.domain),
                SubstVars(Copy(self(o.child(1), y, x, opts)), tab((o.var.id) := (V(its-1)))),

                chain(
                   SubstVars(Copy(self(o.child(1), t, x, opts)), tab((o.var.id) := (V(its-1)))),
                   SubstVars(Copy(self(o.child(1), y, t, opts)), tab((o.var.id) := (V(its-2)))))),

           When(o.domain <= 2, [],
               loop(newind, newind.range,
                   chain(
                       SubstVars(Copy(self(o.child(1), t, y, opts)), tab((o.var.id) := (its-1-peel_its)-2*newind)),
                       SubstVars(Copy(self(o.child(1), y, t, opts)), tab((o.var.id) := (its-2-peel_its)-2*newind)))))
       ))),

    Multiplication:= meth(self, o, y, x, opts)
        local iterator;

        iterator:=Ind();
        return loop(iterator, [ 0 .. o.element[2]-1 ],
            assign(nth(StripList(y), iterator), mul(nth(x[1], iterator),nth(x[2], iterator))));
    end,

    OLMultiplication := meth(self, o, y, x, opts)
        local iterator;
        iterator:=Ind();
        return loop(iterator, [ 0 .. o.rChildren()[2]-1 ],
            assign(nth(StripList(y), iterator),
                ApplyFunc(mul, List([1..o.rChildren()[1]], i -> nth(x[i], iterator)))));
    end,

    OLConjMultiplication := meth(self, o, y, x, opts)
        local iterator;
        iterator:=Ind();
        return loop(iterator, [ 0 .. o.rChildren()[2]-1 ],
            assign(nth(StripList(y), iterator),
                ApplyFunc(mul, [nth(x[1], iterator)] :: List([2..o.rChildren()[1]], i -> conj(nth(x[i], iterator))))));
    end,

    __RCOLMultiplication := (self, o, y, x, conj) >> let(
        i  := Ind(),
        n  := o.rChildren()[2],
        m  := o.rChildren()[1],
        yy := StripList(y),
        re := List([1..m], e -> var.fresh_t("re", yy.t.t)),
        im := List([1..m], e -> var.fresh_t("re", yy.t.t)),
        loop(i, n, decl( re :: im, chain(
            assign( re[1], nth(x[1], 2*i) ),
            assign( im[1], nth(x[1], 2*i+1) ),
            chain( List( [2..m], j ->
                chain(
                    assign(re[j], re[j-1] * nth(x[j],2*i) - conj * im[j-1] * nth(x[j],2*i+1)),
                    assign(im[j], im[j-1] * nth(x[j],2*i) + conj * re[j-1] * nth(x[j],2*i+1))
                ))),
            assign(nth(yy,2*i),   re[m]),
            assign(nth(yy,2*i+1), im[m]))))),

    RCOLMultiplication     := (self, o, y, x, conj) >> self.__RCOLMultiplication(o, y, x, 1),
    RCOLConjMultiplication := (self, o, y, x, conj) >> self.__RCOLMultiplication(o, y, x, -1),

    OLDup := (self, o, y, x, opts) >> let(
        i := Ind(o.params[2]),
        loop(i, i.range, chain(
            List( Flat([y]), yy -> assign(nth(yy, i), nth(x, i)))
        ))
    ),

    SMAP := (self, o, y, x, opts) >> assign(nth(y, 0), o.at(List([1..Cols(o)], i -> nth(x, i-1)))),

    ParSeqWrap := (self, o, y, x, opts) >> let( yy := Flat([y]), xx := Flat([x]),
        self( o.p.child(o.ci),
            StripList(yy :: o.p.filtSUMR(o.y)),
            StripList(xx :: o.p.filtSUMR(o.x)),
            opts)),

    ParSeq := (self, o, y, x, opts) >> let( ch := o.children(), yy := Flat([y]), xx := Flat([x]),
        self( Compose(List([1..Length(ch)], i -> ParSeqWrap(o, i, yy, xx))),
            StripList(o.filtCompR(yy)), StripList(o.filtCompL(xx)), opts)),

    IParSeq := (self, o, y, x, opts) >> let(
            its := Ind(o.domain),
            xs  := o.filtSUML(Flat([x])),
            ys  := o.filtSUMR(Flat([y])),
            xc  := o.filtCompL(Flat([x])),
            yc  := o.filtCompR(Flat([y])),

            rc  := o.filtCompR(Flat([Rows(o)])),
            tc  := List(Zip2(yc, rc), a -> Dat1d(a[1].t.t, a[2])),

            src := List( xc, e -> var.fresh_t("pX", TPtr(e.t.t))),
            dst := List( yc, e -> var.fresh_t("pY", TPtr(e.t.t))),

            ptr := (p) -> When(IsPtrT(p.t), p, nth(p, 0).toPtr(p.t.t)),

            decl(src :: dst :: tc, chain(chain(
                List( TransposedMat([src, xc]),
                    e -> assign(e[1], ptr(e[2])) ) ::
                List( TransposedMat([dst, tc, yc]),
                    e -> assign(e[1], cond( eq(imod(o.domain, 2), 0), ptr(e[2]), ptr(e[3]))) )),
                loopn( its, its.range, chain(
                    self( SubstVars(Copy(o.child(1)), tab((o.var.id) := its)), StripList(dst :: ys), StripList(src :: xs), opts ),
                    chain(
                        List( TransposedMat([src, dst]),
                            e -> assign(e[1], e[2])) ::
                        List( TransposedMat([dst, yc, tc]),
                            e -> assign(e[1], cond( eq(imod(add(o.domain, its), 2), 0), ptr(e[2]), ptr(e[3]) )))
                )))
            ))
    ),

    Cvt := (self, o, y, x, opts) >> o.params[1].code(y, x, opts),
));

StackAllocsToPtrs := function(icode, x, size, vars)
    local arrayvars, replvars, offset, i;

    # extract all temp array definitions
    arrayvars := Flat(List(
        Collect(icode, @(1, decl, e -> ForAny(e.vars, i -> ObjId(i.t) = TArray))),
        f -> Filtered(f.vars, g -> ObjId(g.t) = TArray)
    ));

    # build a set of replacement pointers for these variables
    replvars := List(arrayvars, e -> var.fresh_t("T", TPtr(e.t.t)));

    offset := 2*size;

    for i in [1..Length(arrayvars)] do

        # remove declaration of array
        icode := SubstTopDown(icode, @(1, decl, e -> arrayvars[i] in e.vars), ee -> ee.cmd);

        # change variables from TArray -> TPtr
        icode := SubstTopDown(icode, arrayvars[i], e -> replvars[i]);

        # prepend declaration and setup of ptr
        icode := decl(replvars[i], chain(
            assign(replvars[i], add(x, offset)),
            icode
        ));

        offset := offset + size;
    od;

    Append(vars, arrayvars);

    return icode;
end;

Class(SingleAllocCodegen, DefaultCodegen, rec(
    Formula := meth(self, o, y, x, opts)
        local icode, datas, prog, params, sub, initsub, io, vars, size;

        o := SumsUnification(o.child(1), opts);

        [x, y] := self.initXY(x, y, opts);

        size := Maximum(o.dimensions);
        params := Set(Collect(o, param));

        datas := Collect(o, FDataOfs);
        o := BlockSums(opts.globalUnrolling, o);
        icode := self(o, y, x, opts);
        icode := RemoveAssignAcc(icode);
        icode := BlockUnroll(icode, opts);

        # replace all stack allocated arrays with offsets into input array
        vars := [];
        icode := StackAllocsToPtrs(icode, x, size, vars);

        # icode := PowerOpt(icode);
        icode := DeclareHidden(icode);
        if IsBound(opts.isFixedPoint) and opts.isFixedPoint then
            icode := FixedPointCode(icode, opts.bits, opts.fracbits);
        fi;

        io := When(x=y, [x], [y, x]);
        sub := Cond(IsBound(opts.subName), opts.subName, "transform");
        initsub := Cond(IsBound(opts.subName), Concat("init_", opts.subName), "init");
        icode := func(TVoid, sub, Concatenation(io, params), icode);

        if IsBound(opts.generateInitFunc) and opts.generateInitFunc then
            prog := program(
                decl(List(datas, x->x.var),
                    chain(
                        func(TVoid, initsub, params, chain(List(datas, x -> SReduce(x.var.init)))),
                        icode
                    )));
        else
            prog := program( func(TVoid, initsub, params, chain()), icode);
        fi;
        prog.dimensions := o.dimensions;
        return prog;
    end,
));

Class(RecCodegenMixin, rec(
    RecursStep := (self, o, y, x, opts) >> let(
        name := spiral.libgen.CodeletName(spiral.libgen.CodeletShape(o.child(1))),
        ApplyFunc(call, Concatenation(
                [ var(name), y + o.yofs, x + o.xofs ],
                spiral.libgen.CodeletParams(o.child(1))))),

    RecursStepCall := (self, o, y, x, opts) >>
        ApplyFunc(call, Concatenation(Flat([var(o.func), y, x,]), List(o.bindings, x->x[2]))),

    Codelet := meth(self, o, y, x, opts)
        local code;
        [x, y] := self.initXY(x, y, opts);
        o := o.child(1);
        ## Generating code : main body
        o := BlockSums(opts.libgen.basesUnrolling, o);
        code := SReduce(self(o, y, x, opts), opts);
        code := BlockUnroll(RemoveAssignAcc(code), opts);
        code := DeclareHidden(code);
        return code;
    end,
));


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

# 2-operand intermediate representation.
# Here we define 2-op commands and 3-op -> 2-op
# conversion functions.
#
# This is relevant if one needs very low-level x86 representation,
# eg. to optimize register use and generate assembly code.
#

Class(assign_two_op, assign, rec(
    op_in := self >> Set(ArgsExp(self.exp)),
    op_out := self >> Set([]),
    op_inout := self >> Set([self.loc])
));

Class(assign_add, assign_two_op);
Class(assign_mul, assign_two_op);
Class(assign_sub, assign_two_op);
Class(assign_neg, assign_two_op);

# Examples:
#   assign(t1, add(t1, t2)) == assign_add(t1, t2),
#   assign(t1, add(t2, t3)) == chain(assign(t1, t2), assign_add(t2, t3)).
#

Class(TwoOpCodeRuleSet, RuleSet);
RewriteRules(TwoOpCodeRuleSet, rec(
    add := Rule([assign, @(1), [add, @(2), @(3)]], e -> let(
	    s2:=Length(SuccLoc(@(2).val)), s3:=Length(SuccLoc(@(3).val)),
	When(Length(Filtered(List(@(2).val.succ,x->x.def.n),y->y>@(1).val.def.n))>0,
#	When(IsBound(@(2).val.live_out) or (s3 < s2 and not IsBound(@(3).val.live_out)),
	    chain(assign(@(1).val, @(3).val), # copy+destroy @(3)
		  assign_add(@(1).val, @(2).val)),
	    chain(assign(@(1).val, @(2).val), # copy+destroy @(2)
		  assign_add(@(1).val, @(3).val))))),

    mul := Rule([assign, @(1), [mul, @(2), @(3)]], e -> let(
	    s2:=Length(SuccLoc(@(2).val)), s3:=Length(SuccLoc(@(3).val)),
	When(Length(Filtered(List(@(2).val.succ,x->x.def.n),y->y>@(1).val.def.n))>0,
#	When(IsBound(@(2).val.live_out) or (s3 <= s2 and not IsBound(@(3).val.live_out)),
	    chain(assign(@(1).val, @(3).val), # copy+destroy @(3)
		  assign_mul(@(1).val, @(2).val)),
	    chain(assign(@(1).val, @(2).val), # copy+destroy @(2)
		  assign_mul(@(1).val, @(3).val))))),

    sub := Rule([assign, @(1), [sub, @(2), @(3)]], e ->
	    chain(assign(@(1).val, @(2).val),
		  assign_sub(@(1).val, @(3).val))),

    neg := Rule([assign, @(1), [neg, @(2)]], e ->
            chain(assign(@(1).val, @(2).val),
	          assign_neg(@(1).val)))
));

ElimRedundantCopyChain := function(code)
    local varmap, c, newcmds, succs;
    varmap := tab();
    newcmds := [];
    for c in code.cmds do
        c := SubstVars(c, varmap);
        if ObjId(c)=assign and ObjId(c.loc)=var and ObjId(c.exp)=var and
	   not IsBound(c.exp.live_out) and
	   Filtered(SuccLoc(c.exp), x->x<>c.loc and x.def.n > c.n)=[] then
	    varmap.(c.loc.id) := c.exp;
	else
	    Add(newcmds, c);
	fi;
    od;
    return chain(newcmds);
end;

# Convert three-operand code -> two-operand code
#
TwoOpCode := function(code)
    # NOTE: write a better binsplit
    code := BinSplit(BinSplit(BinSplit(code)));
    MarkLiveness(code);
    code := TwoOpCodeRuleSet(code);
    code := FlattenCode(code);
    MarkLiveness(code);
#    code := ElimRedundantCopyChain(code);
#    code := CopyPropagate(code);
    return code;
end;

X86Code := function(code, numregs)
    local dims;
    if IsBound(code.dimensions) then dims := code.dimensions; fi;
    MarkLiveness(code);
    code := HashConsts(code, rec(declareConstants := true));
    code := TwoOpCode(code);
    MarkLiveness(code);
    code := RegAlloc(code, numregs, TDouble);
    code := DeclareHidden(code);
    if IsBound(dims) then code.dimensions := dims; fi;
    return code;
end;

# assumes <code> is a chain of assigns
TwoOpSSA := function(code)
    local varmap, c, newcmds, succs, a, b, sa, sb, so, v, overwrite, antidep;
    Constraint(IsChain(code) and ForAll(code.cmds, IsAssign));
    varmap := tab();
    newcmds := [];
    for c in code.cmds do
        c.exp := SubstVars(c.exp, varmap);

        if (ObjId(c.exp) in [add, sub, mul]) and ForAny(c.exp.args, IsVar) then
	    Constraint(Length(c.exp.args)=2);
	    [a,b] := c.exp.args;
	    sa := Filtered(SuccLoc(a), x -> x.def.n > c.n);
	    sb := Filtered(SuccLoc(b), x -> x.def.n > c.n);

	    if not IsVar(b) then overwrite := a;
	    elif not IsVar(a) then overwrite := b;
	    elif ObjId(c.exp)=sub then overwrite :=a;
	    elif IsBound(b.live_out) or
		 ((Length(sa) < Length(sb)) and not IsBound(a.live_out)) then
		overwrite := a;
	    else
		overwrite := b;
	    fi;
	    antidep := false;
	    so := When(overwrite=a, sa, sb);
	    if so <> [] then
		v := overwrite.clone();
		v.succ := so;
		overwrite.succ := Difference(overwrite.succ, so);
		Add(newcmds, assign(v, overwrite));
		varmap.(overwrite.id) := v;
		antidep := v;
	    fi;
	    if antidep<>false then c.antidep := [antidep]; fi;
	    Add(newcmds, c);

	else
	    Add(newcmds, c);
	fi;
    od;
    return chain(newcmds);
end;

Class(DirectTwoOpMapping, RuleSet);
RewriteRules(DirectTwoOpMapping, rec(
    add := Rule([assign, @(1), [add, @(2), @(3)]], e ->
	Cond(@(1).val = @(2).val, assign_add(@(2).val, @(3).val),
	     @(1).val = @(3).val, assign_add(@(3).val, @(2).val),
	     Error("Statement ", e, " is not 2-op ready, must be a=a+b, or b=a+b"))),

    mul := Rule([assign, @(1), [mul, @(2), @(3)]], e ->
	Cond(@(1).val = @(2).val, assign_mul(@(2).val, @(3).val),
	     @(1).val = @(3).val, assign_mul(@(3).val, @(2).val),
	     Error("Statement ", e, " is not 2-op ready, must be a=a*b, or b=a*b"))),

    sub := Rule([assign, @(1), [sub, @(2), @(3)]], e ->
	Cond(@(1).val = @(2).val, assign_sub(@(2).val, @(3).val),
	     Error("Statement ", e, " is not 2-op ready, must be a=a-b")))
));
# def b1
# a1: consume b1
# a2: consume b1

# def b1
# cpy b1->b2

# a1: consume b1
# a2: consume b2


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

_VConstructR := [VBase, VRCDiag, BlockVPerm, VPerm, VTensor, VDiag_x_I, VDiag, VGath, VScat, VScatAcc, VPrm_x_I, VGath_sv, VBlk, VContainer, RCVDiag, VO1dsJ, VScale, VGath_zero, VReplicate, VIxJ2, VJxI];
_VConstructL := [VBase, VRCDiag, BlockVPerm, VPerm, VTensor, VDiag_x_I, VDiag, VGath, VScat, VScatAcc, VPrm_x_I, VScat_sv, VScat_svAcc, VScat_pc, VBlk, VContainer, RCVDiag, VReplicate, VO1dsJ, VScale, VGath_zero, VIxJ2, VJxI];

_VTags := [AVecReg, AVecRegCx];

_v_divides_cols := x -> _divides(getV(x), Cols(x));
_v_divides_rows := x -> _divides(getV(x), Rows(x));
_v_tagged_ntR := x->IsNonTerminal(x) and x.hasAnyTag(_VTags) and _divides(x.getAnyTag(_VTags).v, Rows(x));
_v_tagged_ntL := x->IsNonTerminal(x) and x.hasAnyTag(_VTags) and _divides(x.getAnyTag(_VTags).v, Cols(x));

Scat.svVariant := VScat_sv;
ScatAcc.svVariant := VScat_svAcc;
Gath.svVariant := VGath_sv;

Class(RulesVec, RuleSet);
RewriteRules(RulesVec, rec(
    ############################################################
    # Below 2 rules are required by SAR and autolib
    # NOTE: They will cause problems when mixing scalar and vector code,
    # because the entire code (including the scalar portion) will reside inside
    # VContainer, and gath/scat in scalar code will be converted to vgath/vscat
    #
    VContainer__Gath_Scat := Rule(@@(1, [Gath, Scat, ScatAcc], (e,cx)->
        cx.isInside(VContainer) and IsSIMD_ISA(Last(cx.VContainer).isa)
        and let(sg := e, v := Last(cx.VContainer).isa.getV(), vsg := sg.svVariant(sg.func, v, 1), sg.dims() = vsg.dims())
        and (not IsBound(cx.VTensor) or cx.VTensor = [])
        and (not IsBound(cx.VJamL) or cx.VJamL = [])
        and (not IsBound(cx.VJamR) or cx.VJamR = [])
        and (not IsBound(cx.VJam1) or cx.VJam1 = [])
        and (not IsBound(cx.VJam ) or cx.VJam  = [])
        # FF, NOTE: this is a terrible hack to solve a contradiction: How to prevent the rader perm
	#            from turning into VGath. Need to find a better solution.
        and (not ObjId(e.func) in [RM, RR])),
     (e,cx) -> let(
	 #inside_vrc := (IsBound(cx.vRC) and cx.vRC <> []) or
	 #              (IsBound(cx.opts.assumeOuter_vRC) and cx.opts.assumeOuter_vRC),
	 v := Last(cx.VContainer).isa.getV(), # / Cond(inside_vrc, 2, 1),
	 # YSV: isa.getV() now takes care of 'inside_vrc', which is thus commented out,
	 #      and can be removed.
         e.svVariant(e.func, v, 1))),

    VContainer__Diag := Rule(@@(1, [Diag], (e,cx) ->
        cx.isInside(VContainer) and IsSIMD_ISA(Last(cx.VContainer).isa)
        and not cx.isInside(VTensor)
        and not cx.isInside("VJamL")
        and not cx.isInside("VJamR")
        and not cx.isInside("VJam1")
        and not cx.isInside("VJam")
	and let(
	    #inside_vrc := cx.isInside(vRC) or (IsBound(cx.opts.assumeOuter_vRC) and cx.opts.assumeOuter_vRC),
	    v := Last(cx.VContainer).isa.getV(), # / Cond(inside_vrc, 2, 1),
	    Rows(e) mod v = 0)),
     (e,cx) -> let(
	 #inside_vrc := (IsBound(cx.vRC) and cx.vRC <> []) or
	 #              (IsBound(cx.opts.assumeOuter_vRC) and cx.opts.assumeOuter_vRC),
	 v := Last(cx.VContainer).isa.getV(), # / Cond(inside_vrc, 2, 1),
	 # YSV: isa.getV() now takes care of 'inside_vrc', which is thus commented out,
	 #      and can be removed.
         VDiag(e.element, v))),

# YSV: if VContainer is used, then I need the following (in particular the VDiag)
    VContainer_XXX := ARule(Compose, [@(1, VContainer), @(2, [VDiag, Diag, Gath, Scat, ScatAcc])],
        e -> [ CopyFields(@(1).val, rec(_children := [@(1).val._children[1] * @(2).val])) ]),

    XXX_VContainer := ARule(Compose, [@(1, [VDiag, Diag, Gath, Scat, ScatAcc]), @(2, VContainer)],
        e -> [ CopyFields(@(2).val, rec(_children := [@(1).val * @(2).val._children[1]])) ]),

    VContainer_VContainer := Rule( [@(1, VContainer), @(2, VContainer, x -> x.isa = @(1).val.isa and x.isa.isCplx()=@(1).val.isa.isCplx())],
        e -> e.child(1)),

#    Gath_Scat_ScatGath_XXX := ARule(Compose, [ @(1, Scat), @(2, ScatGath) ],
##    Gath_Scat_ScatGath_XXX := ARule(Compose, [@@(1, [Scat, Gath], (e,cx)->IsBound(cx.VContainer) and cx.VContainer <> []), @(2, ScatGath)],
#        (e, cx)->let(sg := @@(1).val, v := Last(cx.VContainer).v,
#            Concat(Error(Caught), [ sg.svVariant(sg.func, vrc.v, 1)]) )),

#############################
    # Composition of subvector and full vector accesses
    # The result is VScat_sv that has rem=0 guaranteed, i.e., does not use partially filled vectors
    VScat_sv__VScat := ARule(Compose, [@(1,[VScat_sv, VScat_svAcc]), @(2, [VScat, VScatAcc])],
    e -> let(func := @(1).val.func,  vfunc := @(2).val.func,
             v    := getV(@(1).val), sv    := @(1).val.sv,
	     oid  := Cond(ObjId(@(1).val)=VScat_svAcc or ObjId(@(2).val)=VScatAcc, VScat_svAcc, VScat_sv),
        [ oid(fCompose(func, fTensor(vfunc, fId(v/sv))), v, sv, 0) ])),

    # The result is VGath_sv that has rem=0 guaranteed, i.e., does not use partially filled vectors
    VGath__VGath_sv := ARule(Compose, [@(1,VGath), @(2,VGath_sv)],
    e -> let(func := @(2).val.func,  vfunc := @(1).val.func,
             v    := getV(@(2).val), sv    := @(2).val.sv,
         [ VGath_sv(fCompose(func, fTensor(vfunc, fId(v/sv))), v, sv, 0) ])),

    # Composition of subvector and scalar accesses
    Scat__VScat_sv := ARule(Compose, [@(1, Scat), @(2, VScat_sv)], e -> let(s := @(2).val,
        [ VScat_sv(fCompose(@(1).val.func, fTensor(s.func, fId(s.sv))), s.v, 1, s.rem) ])),

    ScatAcc__VScat_sv := ARule(Compose, [@(1, ScatAcc), @(2, VScat_sv)], e -> let(s := @(2).val,
        [ VScat_svAcc(fCompose(@(1).val.func, fTensor(s.func, fId(s.sv))), s.v, 1, s.rem) ])),

    VGath_sv__Gath := ARule(Compose, [@(1, VGath_sv), @(2, Gath)], e -> let(s := @(1).val,
        [ VGath_sv(fCompose(@(2).val.func, fTensor(s.func, fId(s.sv))), s.v, 1, s.rem) ])),

    # Subvector access --> full vector access
    VScat_sv_to_VScat := Rule(@(1,VScat_sv,e->getV(e)=e.sv), e->VScat(@(1).val.func, @(1).val.v)),
    VGath_sv_to_VGath := Rule(@(1,VGath_sv,e->getV(e)=e.sv), e->VGath(@(1).val.func, @(1).val.v)),
    VScat_svAcc_to_VScatAcc := Rule(@(1,VScat_svAcc,e->getV(e)=e.sv), e->VScatAcc(@(1).val.func, @(1).val.v)),

    # Increase granularity if possible (when function is fId(n) or fTensor(X, fId(n)))
    GathScat_sv_fTensor := Rule([@(1, [VGath_sv, VScat_sv, VScat_svAcc]),
         [@(2,fTensor), ..., [fId, @(3).cond(e -> Gcd(@(1).val.v/@(1).val.sv, EvalScalar(e)) > 1 )]]],
     e -> let(v := @(1).val.v, sv := @(1).val.sv,
          n := EvalScalar(@(3).val),   gcd := Gcd(v / sv, n),
	  rem := Cond(@(1).val.rem _is Unk, Unk(TInt), @(1).val.rem/gcd),
          ObjId(@(1).val)(fTensor(DropLast(@(2).val.children(), 1), fId(n/gcd)), v, sv*gcd, rem))),

    GathScat_sv_HofTensor := Rule([@(1, [VGath_sv, VScat_sv, VScat_svAcc]),
                        [fCompose, @(9, H, e->_divides(@(1).val.sv, e.params[3]) and e.params[4]=1),
			    [@(2,fTensor), ..., [fId, @(3).cond(e -> Gcd(@(1).val.v/@(1).val.sv, EvalScalar(e)) > 1)]]]],
     e -> let(v := @(1).val.v, sv := @(1).val.sv,
          n := EvalScalar(@(3).val),   gcd := Gcd(v / sv, n), hp := @(9).val.params,
	  rem := Cond(@(1).val.rem _is Unk, Unk(TInt), @(1).val.rem/gcd),
          ObjId(@(1).val)(fCompose(H(hp[1]/gcd, hp[2]/gcd, hp[3]/gcd, 1),  fTensor(DropLast(@(2).val.children(), 1), fId(n/gcd))), v, sv*gcd, rem))),

    GathScat_sv_fId := Rule([@(1, [VGath_sv, VScat_sv, VScat_svAcc]),
	                         [fId, @(3).cond(e -> Gcd(@(1).val.v/@(1).val.sv, EvalScalar(e)) = @(1).val.v)]],
     e -> let(v := @(1).val.v, sv := @(1).val.sv,
          n := EvalScalar(@(3).val),   gcd := Gcd(v / sv, n),
	  rem := Cond(@(1).val.rem _is Unk, Unk(TInt), @(1).val.rem/gcd),
          ObjId(@(1).val)(fId(n/gcd), v, sv*gcd))),

    # Convert objects that are adjacent to vector constructs to vector objects
    #

    VConstruct_Gath := ARule(Compose, [ @(1, _VConstructL), @(2, Gath, e -> _v_divides_cols(@(1).val)) ],
        e -> [ @(1).val, VGath_sv(@(2).val.func, getV(@(1).val), 1) ]),
    VConstruct_Scat := ARule(Compose, [ @(1, _VConstructL), @(2, Scat, e -> _v_divides_cols(@(1).val)) ],
        e -> [ @(1).val, VScat_sv(@(2).val.func, getV(@(1).val), 1) ]),
    VConstruct_ScatAcc := ARule(Compose, [ @(1, _VConstructL), @(2, ScatAcc, e -> _v_divides_cols(@(1).val)) ],
        e -> [ @(1).val, VScat_svAcc(@(2).val.func, getV(@(1).val), 1) ]),
    VConstruct_Diag := ARule(Compose, [ @(1, _VConstructL), [Diag, @(2).cond(e -> _v_divides_cols(@(1).val))] ],
        e -> [ @(1).val, VDiag(@(2).val, getV(@(1).val)) ]),

    Scat_VConstruct := ARule(Compose, [ @(1, Scat), @(2, _VConstructR, _v_divides_rows) ],
        e -> [ VScat_sv(@(1).val.func, getV(@(2).val), 1), @(2).val ]),
    ScatAcc_VConstruct := ARule(Compose, [ @(1, ScatAcc), @(2, _VConstructR, _v_divides_rows) ],
        e -> [ VScat_svAcc(@(1).val.func, getV(@(2).val), 1), @(2).val ]),
    Gath_VConstruct := ARule(Compose, [ @(1, Gath), @(2, _VConstructR, _v_divides_rows) ],
        e -> [ VGath_sv(@(1).val.func, getV(@(2).val), 1), @(2).val ]),
    Diag_VConstruct := ARule(Compose, [ [Diag, @(1)], @(2, _VConstructR, _v_divides_rows) ],
        e -> [ VDiag(@(1).val, getV(@(2).val)), @(2).val ]),

    VGath_Prm := ARule(Compose, [ @(1, VGath), @(2, Prm) ],
        e -> [ @(1).val, VGath_sv(@(2).val.func, getV(@(1).val), 1) ]),
    Prm_VScat := ARule(Compose, [ @(1, Prm), @(2, VScat) ],
        e -> [ VScat_sv(@(1).val.func.transpose(), getV(@(2).val), 1), @(2).val ]),
    Prm_VScatAcc := ARule(Compose, [ @(1, Prm), @(2, VScatAcc) ],
        e -> [ VScat_svAcc(@(1).val.func.transpose(), getV(@(2).val), 1), @(2).val ]),

    # NOTE: RowVec must be of constant size, fix this limitation
    VRowVec := Rule([@(1, VTensor), @(2, RowVec, e -> not IsSymbolic(e.element.domain()))],
        e -> VTensor(@(2).val.toDiagBlk(), @(1).val.vlen)),

    VColVec := Rule([@(1, VTensor), @(2, ColVec, e -> not IsSymbolic(e.element.domain()))],
        e -> VTensor(@(2).val.toDiagBlk(), @(1).val.vlen)),

    VPrm_x_I := Rule([@(1, VTensor), @(2, Prm)], e->VPrm_x_I(@(2).val.func, @(1).val.vlen)),
    VDiag_x_I := Rule([@(1, VTensor), @(2, Diag)], e->VDiag_x_I(@(2).val.element, @(1).val.vlen)),
    RCDiag_x_I := Rule([@(1, VTensor), @(2, RCDiag)], e->VRCDiag(VDup(@(2).val.element, @(1).val.vlen), @(1).val.vlen)),

    VPrm_x_I_Id := ARule(Compose,[@(1,VPrm_x_I), [Prm, fId]],e->[@(1).val]),
    Id_VPrm_x_I := ARule(Compose, [[Prm, fId], @(1,VPrm_x_I)], e->[@(1).val]),

    # Remove identity gathers / scatters
    RemGSP := Rule([@(1, [Gath, Scat, Prm, VGath, VScat]), @(2, fId)], e->I(@(1).val.dims()[1])),
    RemIL  := ARule(Compose, [ @(1), @(2, I) ], e -> [ @(1).val ]),
    RemIR  := ARule(Compose, [ @(1, I), @(2) ], e -> [ @(2).val ]),

    #   H rules
    VGath_sv_H := Rule( [@(1,VGath_sv), [H,
            @(2).cond(e->not IsSymbolic(e)),
            @(3).cond(e->_divides(getV(@(1).val), e)),
            @(4).cond(e->_divides(getV(@(1).val), e)), _1]],
	e -> let(
            d := e.v / e.sv,
	    rmod := EvalScalar(@(2).val) mod getV(@(1).val),
	    Cond(rmod=0,
		 VGath(H(@(2).val/d, @(3).val/d, @(4).val/d, 1), e.v),
		 VGath_pc(@(2).val*e.sv, @(3).val*e.sv, @(4).val*e.sv, e.v))
	)),

    VScat_sv_H := Rule( [@(1, [VScat_sv, VScat_svAcc]), [H,
            @(2).cond(e->not IsSymbolic(e)),
            @(3).cond(e->_divides(getV(@(1).val), e)),
            @(4).cond(e->_divides(getV(@(1).val), e)), _1]],
	e -> let(
            d := e.v / e.sv,
	    oid   := Cond(ObjId(@(1).val)=VScat_sv, VScat, VScatAcc),
	    oidpc := Cond(ObjId(@(1).val)=VScat_sv, VScat_pc, VScat_pcAcc),
	    rmod := EvalScalar(@(2).val) mod getV(@(1).val),
	    Cond(rmod=0,
		 oid(H(@(2).val/d, @(3).val/d, @(4).val/d, 1), e.v),
		 oidpc(@(2).val*e.sv, @(3).val*e.sv, @(4).val*e.sv, e.v))
	)),

   Scat_VScat_pc_to_VScat_sv := ARule(Compose, [@(1, Scat), @(2, VScat_pc, x -> x.N=x.n and x.ofs = 0)],
       e -> [VScat_sv(@(1).val.func, @(2).val.v, 1)]),

   VGath_pc_Gath_H := ARule(Compose, [@(1, VGath_pc), [Gath, [@(2, H), @, @, @, _1]]],
       e -> [VGath_pc(@(2).val.params[1], @(1).val.n, @(2).val.params[3], @(1).val.v)] ),

   Scat_H_VScat_pc := ARule(Compose, [[Scat, [@(1, H), @, @, @, _1]], @(2, [VScat_pc, VScat_pcAcc])],
       e -> [ObjId(@(2).val)(@(1).val.params[1], @(2).val.n, @(1).val.params[3], @(2).val.v)] ),

   ScatGath := Rule([@(1, VTensor), @(2, ScatGath)],
       e -> ScatGath(fTensor(@(2).val.sfunc, fId(@(1).val.vlen)), fTensor(@(2).val.gfunc, fId(@(1).val.vlen)))),

   Scat_Cvt := ARule(Compose, [@(1, Scat), @(2, Cvt, x -> IsSIMD_ISA(x.params[1].isa_to) and _divides(x.params[1].isa_to.v, Cols(@(1).val)) )],
      e -> let(isa := @(2).val.params[1].isa_to, [ VContainer(VScat_sv(@(1).val.func, isa.v, 1), isa), @(2).val ])),
   Cvt_Gath := ARule(Compose, [@(1, Cvt, x -> IsSIMD_ISA(x.params[1].isa_from)), @(2, Gath, x -> _divides(@(1).val.params[1].isa_from.v, Rows(x)))],
      e -> let(isa := @(1).val.params[1].isa_from, [ @(1).val, VContainer(VGath_sv(@(2).val.func, isa.v, 1), isa) ])),
));


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

Declare(sve_gath, sve_scat, sve_ld2, sve_st2);

Class(TVectSVE, TReal);
TVectSVEx2 := TVect(TVectSVE, 2);

Class(TBoolSVE, TBool);
Class(TInt64SVE, TInt);

# SVE vector loop
Class(sve_loopn, loopn);

Class(sve_svcntd, Exp, rec(
    computeType := self >> TInt
));

# svfloat64_t svld1_gather_[u64]offset[_f64](svbool_t pg, const float64_t *base, svuint64_t offsets)

Class(sve_gath, Loc, rec(
    __call__ := (self, loc, n, pg, stride) >> WithBases(self,
        rec(operations := NthOps,
            loc := toExpArg(loc),
            n := toExpArg(n),
            pg := toExpArg(pg),
            stride := toExpArg(stride))).setType(),

    can_fold := False,

    rChildren := self >> [self.loc, self.n, self.pg, self.stride],
    rSetChild := rSetChildFields("loc", "n", "pg", "stride"),

    ev := self >> let(e := self.eval(), Cond(IsBound(e.v), e.v, e)),

    eval := self >> sve_gath(self.loc.eval(), self.n.eval(), self.pg.eval(), self.stride.eval()),

    computeType := self >> TVectSVE,

    isExpComposite := true
));

Class(sve_scat, Command, rec(
   __call__ := (self, loc, n, pg, stride, exp) >> WithBases(self,
       rec(operations := CmdOps,
       loc := toAssignTarget(loc),
       n := toExpArg(n),
       pg := toExpArg(pg),
       stride := toExpArg(stride),
       exp := toExpArg(exp)
       )),

   rChildren := self >> [self.loc, self.n, self.pg, self.stride, self.exp],
   rSetChild := rSetChildFields("loc", "n", "pg", "stride", "exp"),
   unroll := self >> self,

   print := (self,i,si) >> let(name := Cond(IsBound(self.isCompute) and self.isCompute,
                                            gap.colors.DarkYellow(self.__name__),
                                            IsBound(self.isLoad) and self.isLoad,
                                            gap.colors.DarkRed(self.__name__),
                                            IsBound(self.isStore) and self.isStore,
                                            gap.colors.DarkGreen(self.__name__),
                                            self.__name__),
                                Print(name, "(", self.loc, ", ", self.n, ", ", self.pg, ", ", self.stride, ", ", self.exp, ")"))
));

# svfloat64x2_t svld2[_f64](svbool_t pg, const float64_t *base)

Class(sve_ld2, Loc, rec(
    __call__ := (self, loc, n, pg) >> WithBases(self,
        rec(operations := NthOps,
            loc := toExpArg(loc),
            n := toExpArg(n),
            pg := toExpArg(pg))).setType(),

    can_fold := False,

    rChildren := self >> [self.loc, self.n, self.pg],
    rSetChild := rSetChildFields("loc", "n", "pg"),

    ev := self >> let(e := self.eval(), Cond(IsBound(e.v), e.v, e)),

    eval := self >> sve_ld2(self.loc.eval(), self.n.eval(), self.pg.eval()),

    computeType := self >> TVectSVEx2,

    isExpComposite := true
));

Class(sve_st2, Command, rec(
   __call__ := (self, loc, n, pg, exp) >> WithBases(self,
       rec(operations := CmdOps,
       loc := toAssignTarget(loc),
       n := toExpArg(n),
       pg := toExpArg(pg),
       exp := toExpArg(exp)
       )),

   rChildren := self >> [self.loc, self.n, self.pg, self.exp],
   rSetChild := rSetChildFields("loc", "n", "pg", "exp"),
   unroll := self >> self,

   print := (self,i,si) >> let(name := Cond(IsBound(self.isCompute) and self.isCompute,
                                            gap.colors.DarkYellow(self.__name__),
                                            IsBound(self.isLoad) and self.isLoad,
                                            gap.colors.DarkRed(self.__name__),
                                            IsBound(self.isStore) and self.isStore,
                                            gap.colors.DarkGreen(self.__name__),
                                            self.__name__),
                                Print(name, "(", self.loc, ", ", self.n, ", ", self.pg, ", ", self.exp, ")"))
));


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

# Declaration of a sorter of one dimension within tuples of size n
#n is the number of inputs in total. So, there are n/2 tuples
#w should be minimum 2
Class(SortVecBase, BaseMat, rec(
   abbrevs   := [()-> []],
   new       := (self) >> SPL( WithBases(self, rec()) ).setDims(),
   dims      := self >> [ 4, 4 ],
   isReal    := True,
   sums     := self >> self,
   rChildren := self >> [],
   rSetChild := rSetChildFields(),
   toAMat := self >> Error("not supported"),
   transpose := self >> self,
));

Class(SortVecConfigBase_w2, BaseMat, rec(
   abbrevs   := [(a)-> [a] , (b)-> [b]],
   new       := (self, a, b) >> SPL( WithBases(self, rec(dimensions:=[2,2], a := a, b:=b))),
   print := (self, i, is) >> Print(self.name, "(", self.a, ",", self.b, ")"),
   dims      := self >> [ 2, 2 ],
   isReal    := True,
   sums     := self >> self,
   rChildren := self >> [self.a, self.b],
   rSetChild := rSetChildFields("a","b"),
   toAMat := self >> Error("not supported"),
   transpose := self >> self,
));

# Declaration of SortConfigBase, a 2x2 Configurable sorter.
Class(SortVecConfigBase, BaseMat, rec(
   abbrevs   := [(a)-> [a]],
   new       := (self, a) >> SPL( WithBases(self, rec(dimensions:=[4,4], a := a))),
   print := (self, i, is) >> Print(self.name, "(", self.a, ")"),
   dims      := self >> [ 4, 4 ],
   isReal    := True,
   sums     := self >> self,
   rChildren := self >> [self.a],
   rSetChild := rSetChildFields("a"),
   toAMat := self >> Error("not supported"),
   transpose := self >> self,
));

Class(SortVecBase_w2, BaseMat, rec(
   abbrevs   := [(a)-> [a]],
   new       := (self, a) >> SPL( WithBases(self, rec(dimensions:=[2,2], a := a))),
   print := (self, i, is) >> Print(self.name, "(", self.a, ")"),
   dims      := self >> [ 2, 2 ],
   isReal    := True,
   sums     := self >> self,
   rChildren := self >> [self.a],
   rSetChild := rSetChildFields("a"),
   toAMat := self >> Error("not supported"),
   transpose := self >> self,
));

HDLCodegen.SortVecBase_w2 := (self, o, y, x, opts) >>
   let(
     t0 := TempVar(x.t.t),
     t1 := TempVar(x.t.t),
     t3 := TempVar(x.t.t),
     t5 := TempVar(x.t.t),
     t6 := TempVar(x.t.t),

     t20 := TempVar(x.t.t),
     t21 := TempVar(x.t.t),
     t23 := TempVar(x.t.t),
     t25 := TempVar(x.t.t),
     t26 := TempVar(x.t.t),

     chain(
        assign(t3, imod(o.a, 2)),
        regassign(t0, cond(t3, t0, nth(x,0))),
        assign(t5, cond(leq(t0, nth(x,0)), nth(x,0), t0)),
        assign(t6, cond(leq(t0, nth(x,0)), t0, nth(x,0))),
        regassign(t1, cond(t3, t5, t1)),
        assign(nth(y,0), cond(t3, t6, t1)),

        regassign(t20, cond(t3, t20, nth(x,1))),
        assign(t25, cond(leq(t0, nth(x,0)), nth(x,1), t20)),
        assign(t26, cond(leq(t0, nth(x,0)), t20, nth(x,1))),
        regassign(t21, cond(t3, t25, t21)),
        assign(nth(y,1), cond(t3, t26, t21))

     )
  );

HDLCodegen.SortVecConfigBase_w2 := (self, o, y, x, opts) >>
   let(
     t0 := TempVar(x.t.t),
     t1 := TempVar(x.t.t),
     t3 := TempVar(x.t.t),
     t5 := TempVar(x.t.t),
     t6 := TempVar(x.t.t),
     t7 := TempVar(x.t.t),
     t8 := TempVar(x.t.t),
     t9 := TempVar(x.t.t),
     t10 := TempVar(x.t.t),
     t2 := TempVar(x.t.t),

     t20 := TempVar(x.t.t),
     t21 := TempVar(x.t.t),
     t23 := TempVar(x.t.t),
     t25 := TempVar(x.t.t),
     t26 := TempVar(x.t.t),

     chain(
	assign(t3, imod(o.a, 2)),
	regassign(t0, cond(t3, t0, nth(x,0))),
	assign(t7, eq(o.b,0)),
	assign(t8, eq(o.b,1)),

	assign(t2, leq(t0, nth(x,0))),
	assign(t9, cond(t2, t0, nth(x,0))),
	assign(t10, cond(t2, nth(x,0), t0)),
	assign(t5, cond(t7, nth(x,0) , t8, t9, t10)),
	assign(t6, cond(t7, t0 , t8, t10, t9)),

	regassign(t1, cond(t3, t5, t1)),
	assign(nth(y,0), cond(t3, t6, t1)),

        regassign(t20, cond(t3, t20, nth(x,1))),
        assign(t25, cond(leq(t0, nth(x,0)), nth(x,1), t20)),
        assign(t26, cond(leq(t0, nth(x,0)), t20, nth(x,1))),
        regassign(t21, cond(t3, t25, t21)),
        assign(nth(y,1), cond(t3, t26, t21))

     )
    );

HDLCodegen.SortVecBase := (self, o, y, x, opts) >>
    chain(
        assign(nth(y,0), cond(leq(nth(x,0), nth(x,2)), nth(x,0), nth(x,2))),
        assign(nth(y,2), cond(leq(nth(x,0), nth(x,2)), nth(x,2), nth(x,0))),

        assign(nth(y,1), cond(leq(nth(x,0), nth(x,2)), nth(x,1), nth(x,3))),
        assign(nth(y,3), cond(leq(nth(x,0), nth(x,2)), nth(x,3), nth(x,1)))
    );

HDLCodegen.SortVecConfigBase := (self, o, y, x, opts) >>
    let(
	t0 := TempVar(x.t.t),
	t1 := TempVar(x.t.t),
	t2 := TempVar(x.t.t),
	t3 := TempVar(x.t.t),
	chain(
	    assign(t2, nth(x,0)),
	    assign(t3, nth(x,2)),
	    assign(t0, cond(leq(t2, t3), t2, t3)),
	    assign(t1, cond(leq(t2, t3), t3, t2)),
	    assign(nth(y,0), cond(eq(o.a,0), t2, eq(o.a,1), t1, t0)),
	    assign(nth(y,2), cond(eq(o.a,0), t3, eq(o.a,1), t0, t1)),

            assign(nth(y,1), cond(leq(nth(x,0), nth(x,2)), nth(x,1), nth(x,3))),
            assign(nth(y,3), cond(leq(nth(x,0), nth(x,2)), nth(x,3), nth(x,1)))

	)
    );

Class(SortVec, TaggedNonTerminal, rec(
    abbrevs := [
    (n)       -> Checked(IsPosIntSym(n), [_unwrap(n)]),
    ],

    hashAs := self >> ObjId(self)(self.params[1]).withTags(self.getTags()),

    dims := self >> [ self.params[1], self.params[1] ],

    terminate := self >> Error("not supported"), # we could probably support this
));

NewRulesFor(SortVec, rec(

    Sort_Stream_Vec := rec(
        info         := "Streaming sorting network",

        applicable   := nt -> Length(nt.params) = 1 and IsTwoPower(nt.params[1]),

        children := (self, nt) >> let(

	    tag_w_tmp := nt.tags[1],
	    tag_w := tag_w_tmp.bs,
	    t := Log2Int(nt.params[1]),
	    p := Ind(2^t),
	    #get_bb := w -> TTensorI(SortVecBase(), 2^(t-1), APar, APar),
	    get_bb := w -> Cond(w=2, TTensorInd(SortVecBase_w2(p), p, APar, APar),TTensorI(SortVecBase(), 2^(t-1), APar, APar)),

	    [[ TCompose(
	           [TCompose(List([1..t-1], i ->
	               TCompose([
		           #TTensorI(SortBase(), 2^(t-1), APar, APar),
		           get_bb(tag_w),
		           TCompose(List([2..(t-i+1)], j ->
		               TCompose([

			           # The one below *should* work, but it causes some rewriting problems.
			           #TPrm(Tensor(Tensor(I(2^(t-j)),(Tensor(I(2), L(2^(j-1), 2^(j-2))) * L(2^j,2))),I(2))),
				   # So, I'm going to pull the first I() out of the tensor product.  This
				   # changes it from:
				   #      TPrm(I x (IxL)*L x I)
				   # to:
				   #      I x (TPrm(IxL)*L x I)
				   TTensorI(TPrm(Tensor(Tensor(I(2), L(2^(j-1), 2^(j-2))) * L(2^j,2), I(2))), 2^(t-j), APar, APar),

				   # This one doesn't work because it has nested TPrms and it has TTensorI in a TPrm.
			           #TPrm(Tensor(TTensorI(TPrm(Tensor(I(2), L(2^(j-1), 2^(j-2))) * L(2^j,2)), 2^(t-j), APar, APar),I(2))),
			           get_bb(tag_w)
			           #TTensorI(SortBase(), 2^(t-1), APar, APar)
			       ])
		           )),

			   # TPrm(Tensor(TTensorI(TPrm(L(2^(t-i+1), 2^(t-i)) * SortIJPerm(2^(t-i+1))), 2^(i-1), APar, APar),I(2)))
			   # Like above, re-ordering these terms
			   # TPrm(Tensor(Tensor(I(2^(i-1)), L(2^(t-i+1), 2^(t-i)) * SortIJPerm(2^(t-i+1) ) ) ,I(2)))
			   TTensorI(TPrm(Tensor(L(2^(t-i+1), 2^(t-i)) * SortIJPerm(2^(t-i+1)), I(2))), 2^(i-1), APar, APar)

		       ])
	            )),
		    get_bb(tag_w)]
		    #TTensorI(SortBase(), 2^(t-1), APar, APar)]
                ).withTags(nt.getTags())
            ]]
	),

        apply        := (nt, c, cnt) -> c[1],

    ),

   Sort_Stream4_Vec := rec(
        info         := "",

	depth := 1,

        applicable   := (self, nt) >> Length(nt.params) = 1 and IsTwoPower(nt.params[1]) and
	                              let (d := self.depth, t := Log2Int(nt.params[1]), IsInt(t*t/self.depth) and
				      (IsInt(d/t) or IsInt(t/d))),

        children := (self, nt) >> let(
   	    t := Log2Int(nt.params[1]),
	    d := self.depth,
	    d1 := cond(leq(d,t), d, t).ev(),
	    d2 := cond(leq(d,t), 1, d/t).ev(),

	    k := Ind(2^(t-1)),
	    j := Ind(t),
	    l := Ind(t),
	    n := Ind(t/d2),
	    v := Ind(t/d1),

            d_tmp := cond(leq(d,t), t/d, 0).ev(),
            d2_tmp := cond(leq(t,d), d/t, 0).ev(),
	    s_tmp := ((t*t)/d),
            m2 := Ind(d_tmp),
	    s2 := Ind(s_tmp),
	    v2 := Ind(d2_tmp),
	    n2 := Ind(d),
	    n3 := Ind(d),
	    l2 := Ind(t),

	    tag_w_tmp := nt.tags[1],
	    tag_w := tag_w_tmp.bs,
	    p := Ind(2^t),

	    c1 := (lp, jp) >> lt((t-1), (lp+jp)),
	    z := (lp, jp) >> (t-1)-(lp+jp),
	    z_w1 := (lp, jp) >> (t-1)-(lp+jp)+1,
	    c2 := (lp, jp) >> logic_and(eq(bit_sel(k, z(lp, jp)), 1), neq(lp, 0)),
	    c2_w1 := (lp, jp) >> logic_and(eq(bit_sel(p, z_w1(lp, jp)), 1), neq(lp, 0)),

	    access_f := (lp, jp) >> cond(c1(lp, jp), 0, c2(lp, jp), 1, 2),
	    access_f_w1 := (lp, jp) >> cond(c1(lp, jp), 0, c2_w1(lp, jp), 1, 2),

	    get_bb := (lp,jp) -> Cond(tag_w=2, TTensorInd(SortVecConfigBase_w2(p,access_f_w1(lp,jp)), p, APar, APar),TTensorInd(SortVecConfigBase(access_f(lp, jp)), k, APar, APar)),

	    stage := (lp, jp) >> TCompose([
		       get_bb(lp, jp),
		       #TTensorInd(SortConfigBase(access_f(lp, jp)), k, APar, APar),
		       TPrm(Tensor(L(2^t, 2^(t-1)),I(2)))
	           ]),

	    full_stage := np_1 >> TCompose(List([0..t-1], m_1 -> TCompose(List([0..t-1], j_1 -> stage(m_1, j_1))))),
	    full_stage1 := np >> TCompose(List([0..d2-1], m -> TCompose(List([0..t-1], j -> stage(d2*np+m, j))))),
	    full_stage2 := vp >> TCompose(List([0..d1-1], s -> stage(l, vp+s))),
            full_stage1b := np2 >> TICompose(m2,d_tmp, TICompose(j,t, stage((t/d)*np2+m2, j))),

            # Old: problem is that it's assuming the l2 above, which is an unassigned iterator.
	    # There is also a problem with the vp2+s2 parameter: you need to multiply vp2 by the number of iterations.
            # full_stage2b := (vp2) >> TICompose(s2,s_tmp, stage(l2, vp2+s2)),
            full_stage2b := (vp2, l3) >> TICompose(s2,s_tmp, stage(l3, vp2*s_tmp+s2)),

  	    [[ Cond(d=t*t, full_stage(0).withTags(nt.getTags()),
		    d<t, TCompose(List([0..d-1], n2 -> full_stage1b(n2))).withTags(nt.getTags()),
                    d=t, TCompose(List([0..d-1], n3 -> full_stage1b(n3))).withTags(nt.getTags()),
                    d>t, TCompose(List([0..t-1], l3 -> TCompose(List([0..d2_tmp-1], v2 -> full_stage2b(v2, l3))))).withTags(nt.getTags())) #the problem seems to be the outer most TCompose works if it was TICompose
		    #d>t, TICompose(n, t/d2, full_stage1(n)).withTags(nt.getTags())) #old one but will leave it as new one does not work yet
	        ]]
	),

#

        apply        := (nt, c, cnt) -> c[1],

    ),

));


SetRecursionTrapInterval(100000);
# No limit (may crash GAP if recursion is not controlled) :
SetRecursionTrapInterval(0);