#############################################################################
##
#W  id384e.gap             GAP library of id's             Hans Ulrich Besche
##

ID_GROUP_TREE.next[384].next[137].next[1].next[16]:=
rec(
  fp:= [ 1476, 2636, 6600, 12238, 14104, 14474, 14980, 18057, 19809, 28110,
31099, 32061, 33987, 36179, 44069, 44294, 45675, 46424, 48838, 57531, 64443,
66649, 68297, 69424, 70887, 75849, 78400, 78938, 80096, 81806, 82147, 83246,
85867, 87468, 89093, 94091, 95053, 99213 ],
  next:= [ rec(
    desc:= [ 108003 ],
    fp:= [ 10, 416, 614, 812 ],
    next:= [ 8918, rec(
      desc:= [ 113011 ],
      fp:= [ 22, 814 ],
      next:= [ 8927, 15548 ] ), rec(
      desc:= [ 302009 ],
      fp:= [ 742, 1081 ],
      next:= [ 8915, rec(
        desc:= [ 206002 ],
        fp:= [ 4, 14 ],
        next:= [ 8897, 8941 ] ) ] ), rec(
      desc:= [ 302008 ],
      fp:= [ 64681, 74821 ],
      next:= [ rec(
        desc:= [ 108003 ],
        fp:= [ 8, 414 ],
        next:= [ 8916, 8913 ] ), 15539 ] ) ] ), rec(
    desc:= [ 109003 ],
    fp:= [ 6, 214, 412 ],
    next:= [ rec(
      desc:= [ 302008, 302012, 110003, 210002 ],
      fp:= [ 2, 12 ],
      next:= [ 9565, 9568 ] ), 9574, rec(
      desc:= [ 302014, 211002 ],
      fp:= [ 4, 212 ],
      next:= [ 9564, 9569 ] ) ] ), rec(
    desc:= [ 109003 ],
    fp:= [ 414, 612 ],
    next:= [ 15444, rec(
      desc:= [ 302006, 302009, 211002 ],
      fp:= [ 214, 412 ],
      next:= [ 7365, 7364 ] ) ] ), rec(
    desc:= [ 107003 ],
    fp:= [ 8, 18, 216, 414, 612 ],
    next:= [ rec(
      desc:= [ 302005, 208002 ],
      fp:= [ 8, 18 ],
      next:= [ 8974, 8997 ] ), 8956, 8958, rec(
      desc:= [ 112011 ],
      fp:= [ 12, 418 ],
      next:= [ rec(
        desc:= [ 302007 ],
        fp:= [ 742, 1081 ],
        next:= [ 8990, rec(
          desc:= [ 114007 ],
          fp:= [ 4, 14 ],
          next:= [ 8981, 15565 ] ) ] ), 8977 ] ), 8989 ] ), rec(
    desc:= [ 108003 ],
    fp:= [ 8, 18, 216, 414, 612 ],
    next:= [ rec(
      desc:= [ 302007, 109003, 209003, 303010, 213003 ],
      fp:= [ 8, 18 ],
      next:= [ 14950, 14949 ] ), 14925, 14936, rec(
      desc:= [ 302008 ],
      fp:= [ 742, 1081 ],
      next:= [ 14961, rec(
        desc:= [ 107003 ],
        fp:= [ 4, 14 ],
        next:= [ 14963, 14929 ] ) ] ), rec(
      desc:= [ 107003 ],
      fp:= [ 216, 612 ],
      next:= [ 14937, 14930 ] ) ] ), rec(
    desc:= [ 106003 ],
    fp:= [ 4, 14 ],
    next:= [ 7444, 15465 ] ), rec(
    desc:= [ 105003 ],
    fp:= [ 16, 412 ],
    next:= [ 15568, 8999 ] ), rec(
    desc:= [ 302008 ],
    fp:= [ 44881, 54682, 55021 ],
    next:= [ rec(
      desc:= [ 208002 ],
      fp:= [ 4, 311 ],
      next:= [ 9680, 9677 ] ), rec(
      desc:= [ 303008 ],
      fp:= [ 2, 82, 181, 261 ],
      next:= [ 9676, 9665, 9679, 9678 ] ), rec(
      desc:= [ 109003 ],
      fp:= [ 4, 14 ],
      next:= [ 9675, 9664 ] ) ] ), rec(
    desc:= [ 107003 ],
    fp:= [ 616, 814, 1012 ],
    next:= [ rec(
      desc:= [ 302007 ],
      fp:= [ 643, 74581 ],
      next:= [ 9622, 9618 ] ), rec(
      desc:= [ 302007 ],
      fp:= [ 544, 64681 ],
      next:= [ 9649, rec(
        desc:= [ 302007 ],
        fp:= [ 4, 183, 282, 381, 461 ],
        next:= [ 9659, 9648, 9654, 9646, 9653 ] ) ] ), 9652 ] ), rec(
    desc:= [ 107003 ],
    fp:= [ 4, 14 ],
    next:= [ rec(
      desc:= [ 302006, 113007, 303009, 214003 ],
      fp:= [ 214, 412 ],
      next:= [ 9668, 9667 ] ), 9660 ] ), rec(
    desc:= [ 114007 ],
    fp:= [ 16, 412 ],
    next:= [ 15466, rec(
      desc:= [ 302006, 302008, 216002 ],
      fp:= [ 14, 212 ],
      next:= [ 7447, 7446 ] ) ] ), rec(
    desc:= [ 302008, 110003, 210003 ],
    fp:= [ 4, 212 ],
    next:= [ 9554, 9559 ] ), rec(
    desc:= [ 110003 ],
    fp:= [ 10, 614, 812 ],
    next:= [ 7326, rec(
      desc:= [ 116011 ],
      fp:= [ 8, 414 ],
      next:= [ rec(
        desc:= [ 302011 ],
        fp:= [ 742, 1081 ],
        next:= [ 15439, 15427 ] ), rec(
        desc:= [ 302010 ],
        fp:= [ 643, 74581 ],
        next:= [ rec(
          desc:= [ 208002 ],
          fp:= [ 214, 412 ],
          next:= [ rec(
            desc:= [ 210002 ],
            fp:= [ 4, 14 ],
            next:= [ 7353, 7338 ] ), rec(
            desc:= [ 108003 ],
            fp:= [ 14, 212 ],
            next:= [ 7305, 7324 ] ) ] ), 7310 ] ) ] ), rec(
      desc:= [ 302010 ],
      fp:= [ 544, 64681 ],
      next:= [ rec(
        desc:= [ 302007 ],
        fp:= [ 3, 281 ],
        next:= [ rec(
          desc:= [ 108003, 302010, 304008, 210004, 40307010 ],
          fp:= [ 1, 641 ],
          next:= [ 7349, 7350 ] ), 7325 ] ), 7322 ] ) ] ), rec(
    desc:= [ 108003 ],
    fp:= [ 8, 18, 414, 612 ],
    next:= [ rec(
      desc:= [ 106003 ],
      fp:= [ 4, 14 ],
      next:= [ 8951, 15554 ] ), 15556, rec(
      desc:= [ 105003 ],
      fp:= [ 4, 14 ],
      next:= [ 15553, 8949 ] ), 8952 ] ), rec(
    desc:= [ 302006, 109003, 209002 ],
    fp:= [ 218, 614 ],
    next:= [ 9469, 9468 ] ), rec(
    desc:= [ 112007 ],
    fp:= [ 4, 14 ],
    next:= [ rec(
      desc:= [ 105003, 302005, 210002 ],
      fp:= [ 14, 212 ],
      next:= [ 9002, 9001 ] ), 15570 ] ), rec(
    desc:= [ 107003 ],
    fp:= [ 12, 418, 616, 814, 1012 ],
    next:= [ 9645, 9580, rec(
      desc:= [ 302006 ],
      fp:= [ 384, 48361 ],
      next:= [ 9592, 9586 ] ), 9619, rec(
      desc:= [ 302007 ],
      fp:= [ 54781, 64582, 64921 ],
      next:= [ 9628, 9630, 9629 ] ) ] ), rec(
    desc:= [ 109003 ],
    fp:= [ 8, 414, 612 ],
    next:= [ rec(
      desc:= [ 302006 ],
      fp:= [ 384, 48361 ],
      next:= [ 9517, rec(
        desc:= [ 211002 ],
        fp:= [ 216, 414 ],
        next:= [ 9536, 9513 ] ) ] ), rec(
      desc:= [ 302006 ],
      fp:= [ 384, 48361 ],
      next:= [ rec(
        desc:= [ 208002 ],
        fp:= [ 8, 414 ],
        next:= [ 9508, 9526 ] ), rec(
        desc:= [ 302012 ],
        fp:= [ 4, 442, 541, 781 ],
        next:= [ 9486, 9540, rec(
          desc:= [ 210002 ],
          fp:= [ 8, 414 ],
          next:= [ 9503, 9516 ] ), 9530 ] ) ] ), rec(
      desc:= [ 302006 ],
      fp:= [ 384, 48361 ],
      next:= [ rec(
        desc:= [ 210002 ],
        fp:= [ 214, 412 ],
        next:= [ 9515, 9514 ] ), rec(
        desc:= [ 209002 ],
        fp:= [ 18, 414 ],
        next:= [ 9518, 9537 ] ) ] ) ] ), rec(
    desc:= [ 108003 ],
    fp:= [ 8, 216, 414, 612 ],
    next:= [ rec(
      desc:= [ 302009 ],
      fp:= [ 544, 64681 ],
      next:= [ 7409, rec(
        desc:= [ 302008 ],
        fp:= [ 461, 18361 ],
        next:= [ rec(
          desc:= [ 302011 ],
          fp:= [ 342, 681 ],
          next:= [ 7408, 7438 ] ), 7407 ] ) ] ), 15446, rec(
      desc:= [ 114011 ],
      fp:= [ 8, 18, 414 ],
      next:= [ 15463, 7388, rec(
        desc:= [ 302008 ],
        fp:= [ 742, 1081 ],
        next:= [ rec(
          desc:= [ 207002 ],
          fp:= [ 18, 414 ],
          next:= [ 7387, 7402 ] ), 7400 ] ) ] ), rec(
      desc:= [ 302006 ],
      fp:= [ 48361, 58341 ],
      next:= [ rec(
        desc:= [ 302012 ],
        fp:= [ 343, 442, 781, 44581 ],
        next:= [ rec(
          desc:= [ 212002 ],
          fp:= [ 2, 12 ],
          next:= [ 7420, 7439 ] ), rec(
          desc:= [ 211002 ],
          fp:= [ 214, 412 ],
          next:= [ 7426, 7428 ] ), 7435, 7432 ] ), rec(
        desc:= [ 212002 ],
        fp:= [ 14, 212 ],
        next:= [ 15452, 15459 ] ) ] ) ] ), rec(
    desc:= [ 302010 ],
    fp:= [ 445, 54781, 64582 ],
    next:= [ rec(
      desc:= [ 215002 ],
      fp:= [ 12, 418, 814 ],
      next:= [ rec(
        desc:= [ 115011 ],
        fp:= [ 12, 814 ],
        next:= [ 15542, 8945 ] ), rec(
        desc:= [ 115007 ],
        fp:= [ 4, 14 ],
        next:= [ 8920, 8921 ] ), rec(
        desc:= [ 115011 ],
        fp:= [ 8, 414 ],
        next:= [ 15544, rec(
          desc:= [ 108003, 112003, 303008, 303010, 214003 ],
          fp:= [ 214, 412 ],
          next:= [ 8936, 8934 ] ) ] ) ] ), rec(
      desc:= [ 209003 ],
      fp:= [ 214, 412 ],
      next:= [ 8943, 8919 ] ), rec(
      desc:= [ 209004 ],
      fp:= [ 6, 214, 412 ],
      next:= [ 8944, 8902, 8908 ] ) ] ), rec(
    desc:= [ 302006, 108003, 208003 ],
    fp:= [ 4, 212 ],
    next:= [ 14967, 14968 ] ), rec(
    desc:= [ 108003 ],
    fp:= [ 8, 216, 414, 612 ],
    next:= [ rec(
      desc:= [ 302008 ],
      fp:= [ 64681, 74821 ],
      next:= [ 8988, rec(
        desc:= [ 208003 ],
        fp:= [ 8, 612 ],
        next:= [ 8992, 8983 ] ) ] ), 15558, rec(
      desc:= [ 302010 ],
      fp:= [ 544, 64681 ],
      next:= [ rec(
        desc:= [ 207002 ],
        fp:= [ 8, 414 ],
        next:= [ 8967, 8973 ] ), 8959 ] ), rec(
      desc:= [ 302010 ],
      fp:= [ 544, 64681 ],
      next:= [ rec(
        desc:= [ 303014 ],
        fp:= [ 263, 362, 461 ],
        next:= [ 8980, rec(
          desc:= [ 211003 ],
          fp:= [ 2, 12 ],
          next:= [ 15561, 15564 ] ), 8991 ] ), rec(
        desc:= [ 208002 ],
        fp:= [ 216, 414 ],
        next:= [ 8987, 8979 ] ) ] ) ] ), rec(
    desc:= [ 108003 ],
    fp:= [ 6, 214, 412 ],
    next:= [ 9566, 9553, 9567 ] ), 9535, rec(
    desc:= [ 108003 ],
    fp:= [ 818, 1016, 1214, 1412 ],
    next:= [ 15547, rec(
      desc:= [ 302008 ],
      fp:= [ 64582, 64921 ],
      next:= [ 8914, 8896 ] ), rec(
      desc:= [ 105003 ],
      fp:= [ 8, 414 ],
      next:= [ 8942, 15552 ] ), 8917 ] ), rec(
    desc:= [ 105003 ],
    fp:= [ 6, 214, 412 ],
    next:= [ 9534, rec(
      desc:= [ 107003 ],
      fp:= [ 8, 414 ],
      next:= [ rec(
        desc:= [ 302007 ],
        fp:= [ 742, 1081 ],
        next:= [ 9498, rec(
          desc:= [ 302014, 112003, 212003 ],
          fp:= [ 6, 412 ],
          next:= [ 9495, 9490 ] ) ] ), rec(
        desc:= [ 302010 ],
        fp:= [ 544, 74821 ],
        next:= [ 9473, 9519 ] ) ] ), rec(
      desc:= [ 302009 ],
      fp:= [ 544, 64681 ],
      next:= [ rec(
        desc:= [ 206002 ],
        fp:= [ 4, 14, 212 ],
        next:= [ 9500, 9533, 9507 ] ), rec(
        desc:= [ 302015 ],
        fp:= [ 263, 362, 621, 16621, 36421 ],
        next:= [ 9523, rec(
          desc:= [ 110003 ],
          fp:= [ 8, 414 ],
          next:= [ 9499, 9509 ] ), 9544, 9532, rec(
          desc:= [ 113003 ],
          fp:= [ 8, 414 ],
          next:= [ 9531, rec(
            desc:= [ 305011, 214005 ],
            fp:= [ 4, 212 ],
            next:= [ 9494, 9489 ] ) ] ) ] ) ] ) ] ), rec(
    desc:= [ 109003 ],
    fp:= [ 10, 218, 416, 614, 812 ],
    next:= [ 7270, 7285, rec(
      desc:= [ 302009 ],
      fp:= [ 742, 1081 ],
      next:= [ rec(
        desc:= [ 207002 ],
        fp:= [ 16, 412 ],
        next:= [ 7288, rec(
          desc:= [ 107003 ],
          fp:= [ 4, 14 ],
          next:= [ 7346, 7265 ] ) ] ), 7286 ] ), rec(
      desc:= [ 302009 ],
      fp:= [ 643, 74581 ],
      next:= [ rec(
        desc:= [ 108003 ],
        fp:= [ 4, 14 ],
        next:= [ 7339, 7269 ] ), 7287 ] ), rec(
      desc:= [ 302006 ],
      fp:= [ 841, 58261 ],
      next:= [ rec(
        desc:= [ 108003 ],
        fp:= [ 4, 14 ],
        next:= [ 15420, 15432 ] ), rec(
        desc:= [ 108003 ],
        fp:= [ 16, 412 ],
        next:= [ 7345, 7314 ] ) ] ) ] ), rec(
    desc:= [ 109003 ],
    fp:= [ 10, 218, 416, 614, 812 ],
    next:= [ 8895, 8899, rec(
      desc:= [ 302010 ],
      fp:= [ 643, 74581 ],
      next:= [ rec(
        desc:= [ 107003 ],
        fp:= [ 4, 14 ],
        next:= [ 8939, 8925 ] ), rec(
        desc:= [ 108003 ],
        fp:= [ 4, 14 ],
        next:= [ 8900, 8898 ] ) ] ), rec(
      desc:= [ 302009 ],
      fp:= [ 1321, 74581 ],
      next:= [ 8940, rec(
        desc:= [ 109003 ],
        fp:= [ 4, 14 ],
        next:= [ 8901, 8922 ] ) ] ), rec(
      desc:= [ 114011 ],
      fp:= [ 22, 814 ],
      next:= [ 8938, rec(
        desc:= [ 107003 ],
        fp:= [ 4, 14 ],
        next:= [ 15538, rec(
          desc:= [ 116007 ],
          fp:= [ 4, 14 ],
          next:= [ 8937, 15549 ] ) ] ) ] ) ] ), rec(
    desc:= [ 302006, 108003, 208003 ],
    fp:= [ 4, 212 ],
    next:= [ 14924, 14922 ] ), rec(
    desc:= [ 302009 ],
    fp:= [ 445, 54781, 64582 ],
    next:= [ rec(
      desc:= [ 110003 ],
      fp:= [ 10, 614, 812 ],
      next:= [ 7343, rec(
        desc:= [ 116011 ],
        fp:= [ 8, 414 ],
        next:= [ rec(
          desc:= [ 206002 ],
          fp:= [ 4, 14 ],
          next:= [ 15430, 15436 ] ), rec(
          desc:= [ 206002 ],
          fp:= [ 4, 14 ],
          next:= [ 7308, 7330 ] ) ] ), rec(
        desc:= [ 209002 ],
        fp:= [ 2, 111 ],
        next:= [ 7341, 7340 ] ) ] ), rec(
      desc:= [ 111003, 211003 ],
      fp:= [ 216, 414 ],
      next:= [ 7342, 7344 ] ), rec(
      desc:= [ 302007 ],
      fp:= [ 4, 381 ],
      next:= [ 7355, rec(
        desc:= [ 112003, 212004 ],
        fp:= [ 6, 412 ],
        next:= [ 7299, 7309 ] ) ] ) ] ), rec(
    desc:= [ 107003 ],
    fp:= [ 8, 18, 216, 414, 612 ],
    next:= [ rec(
      desc:= [ 105003 ],
      fp:= [ 18, 414 ],
      next:= [ rec(
        desc:= [ 302006, 210002 ],
        fp:= [ 4, 14 ],
        next:= [ 14960, 14958 ] ), rec(
        desc:= [ 302007 ],
        fp:= [ 483, 58261 ],
        next:= [ 14948, 14959 ] ) ] ), 14955, 14938, rec(
      desc:= [ 105003 ],
      fp:= [ 18, 414 ],
      next:= [ 14952, rec(
        desc:= [ 302007 ],
        fp:= [ 483, 58261 ],
        next:= [ 14927, rec(
          desc:= [ 302007 ],
          fp:= [ 5, 481 ],
          next:= [ rec(
            desc:= [ 210002 ],
            fp:= [ 4, 14 ],
            next:= [ 14964, 14962 ] ), 14951 ] ) ] ) ] ), rec(
      desc:= [ 302006 ],
      fp:= [ 483, 58261 ],
      next:= [ rec(
        desc:= [ 208002 ],
        fp:= [ 214, 412 ],
        next:= [ 14942, 14945 ] ), rec(
        desc:= [ 211002 ],
        fp:= [ 14, 113, 212 ],
        next:= [ 14935, 14947, 14939 ] ) ] ) ] ), rec(
    desc:= [ 106003 ],
    fp:= [ 8, 414, 612 ],
    next:= [ 8998, rec(
      desc:= [ 111011 ],
      fp:= [ 12, 418 ],
      next:= [ 15566, 8975 ] ), 8994 ] ), rec(
    desc:= [ 108003 ],
    fp:= [ 8, 18, 414 ],
    next:= [ rec(
      desc:= [ 106003 ],
      fp:= [ 4, 14 ],
      next:= [ 7362, 15442 ] ), 15440, 7358 ] ), rec(
    desc:= [ 109003 ],
    fp:= [ 10, 416, 614, 812 ],
    next:= [ 7319, rec(
      desc:= [ 115011 ],
      fp:= [ 8, 18 ],
      next:= [ 15418, 7267 ] ), rec(
      desc:= [ 302009 ],
      fp:= [ 643, 74581 ],
      next:= [ rec(
        desc:= [ 302007 ],
        fp:= [ 282, 461 ],
        next:= [ 7312, 7317 ] ), 7316 ] ), rec(
      desc:= [ 302009 ],
      fp:= [ 544, 64681 ],
      next:= [ rec(
        desc:= [ 107003 ],
        fp:= [ 8, 414 ],
        next:= [ 7321, 15433 ] ), 7313 ] ) ] ), rec(
    desc:= [ 107003 ],
    fp:= [ 8, 18, 216, 414, 612 ],
    next:= [ rec(
      desc:= [ 302007 ],
      fp:= [ 64681, 74821 ],
      next:= [ rec(
        desc:= [ 207002 ],
        fp:= [ 16, 214 ],
        next:= [ rec(
          desc:= [ 107003 ],
          fp:= [ 16, 412 ],
          next:= [ 7414, 7410 ] ), rec(
          desc:= [ 107003 ],
          fp:= [ 2, 12 ],
          next:= [ 7412, 7401 ] ) ] ), 7419 ] ), 7371, rec(
      desc:= [ 302008 ],
      fp:= [ 643, 1321, 74581 ],
      next:= [ 15447, 7379, 7406 ] ), rec(
      desc:= [ 113011 ],
      fp:= [ 8, 18, 414 ],
      next:= [ rec(
        desc:= [ 302005 ],
        fp:= [ 841, 58261 ],
        next:= [ rec(
          desc:= [ 208002 ],
          fp:= [ 4, 14 ],
          next:= [ 15464, 15462 ] ), rec(
          desc:= [ 209002 ],
          fp:= [ 4, 14, 212 ],
          next:= [ 7442, 7415, 7434 ] ) ] ), 7378, rec(
        desc:= [ 302007 ],
        fp:= [ 742, 1081 ],
        next:= [ rec(
          desc:= [ 206002 ],
          fp:= [ 16, 412 ],
          next:= [ 7373, 7403 ] ), 7382 ] ) ] ), rec(
      desc:= [ 302009 ],
      fp:= [ 544, 64681, 74821 ],
      next:= [ rec(
        desc:= [ 302008 ],
        fp:= [ 342, 441, 681 ],
        next:= [ 7413, rec(
          desc:= [ 208002 ],
          fp:= [ 2, 111 ],
          next:= [ 15453, 15450 ] ), 7436 ] ), rec(
        desc:= [ 207002 ],
        fp:= [ 6, 16 ],
        next:= [ 7411, 7418 ] ), rec(
        desc:= [ 207002 ],
        fp:= [ 16, 412 ],
        next:= [ 7437, 7433 ] ) ] ) ] ), 7323, rec(
    desc:= [ 108003 ],
    fp:= [ 12, 418, 616, 814, 1012 ],
    next:= [ 9638, 9578, rec(
      desc:= [ 302008 ],
      fp:= [ 643, 1321, 74581 ],
      next:= [ rec(
        desc:= [ 302010 ],
        fp:= [ 442, 541, 781, 1021 ],
        next:= [ 9591, rec(
          desc:= [ 215004 ],
          fp:= [ 218, 614, 812 ],
          next:= [ 9603, 9614, 9595 ] ), 9599, 9598 ] ), 9616, 9615 ] ), rec(
      desc:= [ 302008 ],
      fp:= [ 544, 64681 ],
      next:= [ rec(
        desc:= [ 208002 ],
        fp:= [ 3, 112, 211 ],
        next:= [ 9626, 9644, 9642 ] ), rec(
        desc:= [ 108003 ],
        fp:= [ 4, 14 ],
        next:= [ 9627, 9617 ] ) ] ), rec(
      desc:= [ 302008 ],
      fp:= [ 445, 54781, 64582, 64921 ],
      next:= [ 9624, rec(
        desc:= [ 302008 ],
        fp:= [ 3, 182, 281, 361 ],
        next:= [ 9641, 9625, rec(
          desc:= [ 210002 ],
          fp:= [ 3, 112 ],
          next:= [ 9647, 9643 ] ), 9639 ] ), 9631, 9650 ] ) ] ), rec(
    desc:= [ 109003 ],
    fp:= [ 818, 1016, 1214, 1412 ],
    next:= [ 15419, rec(
      desc:= [ 302009 ],
      fp:= [ 445, 54781 ],
      next:= [ 7311, 7315 ] ), 15434, 7320 ] ) ] );


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

domain := f -> f.domain(); pdomain := p -> p.val.domain();
range  := f -> f.range();  prange := p -> p.val.range();
is_perm := f -> f.isPermutation();

Class(fId, PermClass, rec(
    domain := self >> self.params[1],
    range  := self >> self.params[1],
    def    := size -> Checked(IsPosInt0Sym(size), rec(size := size)),
    lambda := self >> let(i := Ind(self.params[1]), Lambda(i,i)),
    transpose := self >> self,
    isIdentity := True
));

Class(fBase, FuncClass, rec(
    abbrevs := [ var -> Checked(IsVar(var) or ObjId(var)=ind, [var.range, var]) ],
    def    := (N, pos) -> rec(),
    domain := self >> 1,
    range  := self >> self.params[1],
    print  := (self, i, is) >> Print(self.name, "(",
        When(ObjId(self.params[2]) in [var, ind], Print(self.params[2]), PrintCS(self.params)), ")"),
    lambda := self >> let(i := Ind(1), Lambda(i, self.params[2]))
));

# used for I_n dirsum J_n
# below represent an alternator <I_n | J_n>_x o fBase(j)
# x = (pos+par) mod 2
Class(fXbase, FuncClass, rec(
    def := (n,j,par,pos) -> rec(),
    domain := self >> 1,
    range := self >> self.params[1],
    lambda := self >> let(
	n:=self.params[1], j:=self.params[2], par:=self.params[3],
	pos:=self.params[4], i := Ind(1),
	Lambda(i, j + imod(pos+par, 2)*(n - 1 - 2*j))
    )
));

# used for I_n dirsum (I_1 dirsum J_n-1)
# below represent an alternator <I_n | I_1 dirsum J_n_1>_x o fBase(j)
# x = (pos+par) mod 2
Class(fYbase, FuncClass, rec(
    def := (n,j,par,pos) -> rec(),
    lambda := self >> let(
	n := self.params[1], j := self.params[2], par := self.params[3],
	pos := self.params[4], i := Ind(1),
	Lambda(i, cond(eq(j, 0), 0, j + imod(pos+par, 2)*(n - 2*j)))
    )
));

Class(fDot, FuncClass, rec(
    def    := (v, func) -> Chain(v.setRange(func.domain()), rec()),
    range  := self >> self.params[2].range(),
    domain := self >> self.params[2].domain(),
    lambda := self >> let(
        v    := self.params[1],
        func := self.params[2],
        i    := Ind(func.domain()),
        Lambda(i, SubstVars(func.at(i), tab((v.id) := i))))
));

Class(fPlaceholder, FuncClass, rec(
    def := func -> rec(),
    range := self >> self.params[1].range(),
    domain := self >> self.params[1].domain(),
    lambda := self >> self.params[1].lambda(),
    at := (self, n) >> self.params[1].at(n)
));

#F fAdd is the "add constant" index-mapping function
#F fAdd(N, n, k): II_n -> II_N, i->i+k
#F References: "Formal Loop Merging for Signal Transforms"
Class(fAdd, FuncClass, rec(
    def := (N, n, val) -> rec(),
    range  := self >> self.params[1],
    domain := self >> self.params[2],
    lambda := self >> let(i := Ind(self.params[2]), Lambda(i, i + self.params[3]))
));

#F fSub is the "subtract constant" index-mapping function
#F fSub(N, n, k): II_n -> II_N, i->i-k
#F References: "fAdd" function
Class(fSub, FuncClass, rec(
    def := (N, n, val) -> rec(N := N, n := n),
    lambda := self >> let(i := Ind(self.params[2]), Lambda(i, i - self.params[3]))
));

Function.hash := false;

Class(FuncClassOper, Function, rec(
    _perm := true,
    _children := [],

    child := (self, n) >> self._children[n],
    children := self >> self._children,
    numChildren := self >> Length(self._children),

    from_rChildren := (self, rch) >> ApplyFunc(ObjId(self), rch),
    rChildren := self >> self._children,
    rSetChild := meth(self, n, what)
        self._children[n] := what;
    end,
    free := self >> Union(List(self.rChildren(), FreeVars)),

    skipOneChild := false,

    advdims   := self >> [ [[ self.range() ]], [[ self.domain() ]] ],
    advrange  := self >> [[ self.range() ]],
    advdomain := self >> [[ self.domain() ]],
    # dimensionality of range (1-d, 2-d, etc)
    advrangeDim := self >> Length(self.advrange()[1]),
    # dimensionality of domain (1-d, 2-d, etc)
    advdomainDim := self >> Length(self.advdomain()[1]),

    __call__ := meth(arg)
        local self, children, lkup, res, h;
        self := arg[1];
        children := Flat(Drop(arg, 1));
	Constraint(ForAll(children, c -> IsFunction(c) or IsFuncExp(c)));
        if self.skipOneChild and Length(children)=1 then return children[1]; fi;

        h := self.hash;
        if h<>false then
            lkup := h.objLookup(self, children);
            if lkup[1]<>false then return lkup[1]; fi;
        fi;
        res := WithBases(self, rec(operations := RewritableObjectOps, _children := children));
        if h<>false then return h.objAdd(res, lkup[2]);
        else return res;
        fi;
    end,

    print := self >> Print(self.name, "(", PrintCS(self._children), ")"),
    range := self >> Error("not implemented"),
    domain := self >> Error("not implemented"),

    rightBinary := self >> FoldR1(self._children, (p,x) -> let(base:=self.__bases__[1], base(x, p))),
    leftBinary  := self >> FoldL1(self._children, (p,x) -> let(base:=self.__bases__[1], base(p, x))),

    equals := (self, o) >> ObjId(o) = ObjId(self) and self.rChildren() = o.rChildren(),

    lessThan := (self, o) >> Cond(
        ObjId(self) <> ObjId(o), ObjId(self) < ObjId(o),
        [ ObjId(self), self.rChildren() ] < [ ObjId(o), o.rChildren() ]
    ),
));

FuncClass.tensor_op := (v,range,domain,fl,gl) ->
    Lambda(v, range * fl.at(idiv(v, domain)) + gl.at(imod(v, domain)));

FuncClassOper.tensor_op := FuncClass.tensor_op;

Class(fTensorBase, SumsBase, FuncClassOper, rec(
    domain := self >> Product(self._children, x->x.domain()),

    range := self >> let(cht := List(self._children, x->x.range()),
        Cond(ForAll(cht, x->not IsType(x)), Product(cht),
             ForAll(cht, x->not IsType(x) or x=TInt), TInt,
             Error("self.range() can't handle this combination of self._children ranges"))),

    skipOneChild := true,

    split_op := self >> let(
        c := self.children(), v := Ind(self.domain()),
        c1dom := c[1].domain(), c2dom := c[2].domain(),
        Checked(Length(c)=2,
            [ Cond(c1dom=1, 0, c2dom=1, v, idiv(v, c2dom)),
              Cond(c1dom=1, v, c2dom=1, 0, imod(v, c2dom)),
              v ])),

    lambda := self >> Cond(
	self.numChildren() > 2,
	    self.rightBinary().lambda(),
        let(split := self.split_op(),
            f := self.child(1).lambda(),
            g := self.child(2).lambda(),
            jv := DropLast(When(Length(f.vars) > Length(g.vars), f.vars, g.vars), 1),
            Lambda(Concatenation(jv, [split[3]]),
                   self.combine_op(jv, split, f, g)))),

    isIdentity := self >> ForAll(self._children, IsIdentity),
));

Class(fTensor, fTensorBase, rec(
    combine_op := (self, jv, split, f, g) >>
        self.child(2).range() * f.relaxed_at(jv, split[1]) + g.relaxed_at(jv, split[2]),

    transpose := self >> self.__bases__[1](List(self.children(), c->c.transpose()))
));

Class(diagTensor, fTensorBase, rec(
    print := FuncClassOper.print,
    range := self >> UnifyTypes(List(self.children(), x->x.range())),
    combine_op := (self, jv, split, f, g) >> f.relaxed_at(jv, split[1]) * g.relaxed_at(jv, split[2])
));

Class(gammaTensor, fTensorBase, rec(
    isCyclic := true,
    print := FuncClassOper.print,
    combine_op := (self, jv, split, f, g) >> let(
        r := self.child(1).range(), s := self.child(2).range(),
        imod(s * no_mod(f.relaxed_at(jv,split[1])) + r * no_mod(g.relaxed_at(jv,split[2])), self.range()))
));

_rankedLambdaCompose := function(l1, l2)
    local jv, v, vars;
    l1.expr := l1.expr.eval();
    l2.expr := l2.expr.eval();

    vars := When(Length(l1.vars) > Length(l2.vars), l1.vars, l2.vars);
    v := Last(vars);
    jv := vars{[1..Length(vars)-1]};

    return Lambda(vars, l1.relaxed_at(jv, l2.relaxed_at(jv, v)));
end;

Class(fCompose, FuncClassOper, rec(
    domain := self >> Last(self._children).domain(),
    range := self >> self._children[1].range(),
    advdomain := self >> Last(self._children).advdomain(),
    advrange := self >> self._children[1].advrange(),
    skipOneChild := true,

    lambda := self >>
        FoldL1(List(self.children(), z->z.lambda()), _rankedLambdaCompose),

    transpose := self >> self.__bases__[1](List(Reversed(self.children()), c->c.transpose())),

    isIdentity := self >> ForAll(self._children, IsIdentity),
));

Class(fDirsum, FuncClassOper, rec(
    domain := self >> Sum(self._children, x->x.domain()),
    range  := self >> Sum(self._children, x->x.range()),
    skipOneChild := true,

    lambda := meth(self)
        local dom_spans, row_spans, i, conds, v;
        # spans uses 1-based offsets
        dom_spans := BaseOverlap.spans(0, List(self.children(), x->x.domain())) - 1;
        row_spans := BaseOverlap.spans(0, List(self.children(), x->x.range())) - 1;
        conds := [];
        v := Ind(self.domain());
        for i in [1..self.numChildren()] do
            Add(conds, leq(dom_spans[i][1], v, dom_spans[i][2]));
            Add(conds, row_spans[i][1] + self.child(i).lambda().at(v - dom_spans[i][1]));
        od;
        return Lambda(v, ApplyFunc(cond, conds));
    end,

    transpose := self >> self.__bases__[1](List(self.children(), c->c.transpose()))
));

Class(fStack, FuncClassOper, rec(
    domain := self >> Sum(self._children, x->x.domain()),
    # NB: use lowercase max that handles symbolics
    range := self >> ApplyFunc(max, List(self._children, x->x.range())),
    subdomainsDivisibleBy := (self, n) >> ForAll(self._children, x -> x.domain() mod n = 0),
    skipOneChild := true,

    lambda := meth(self)
        local dom_spans, i, conds, v;
        # spans uses 1-based offsets
        dom_spans := BaseOverlap.spans(0, List(self.children(), x->x.domain())) - 1;
        conds := [];
        v := Ind(self.domain());
        for i in [1..self.numChildren()] do
            Add(conds, leq(dom_spans[i][1], v, dom_spans[i][2]));
            Add(conds, self.child(i).lambda().at(v - dom_spans[i][1]));
        od;
	# Else branch needed below (=errExp), it ensures that cond(..).ev() does not crash
	# with nested conds. For example:
	#   cond(c1, cond(...., errExp), b)
	# if c1 is false, our inner cond can be invalid, because then b is taken instead
	Add(conds, errExp(conds[2].t));
        return Lambda(v, ApplyFunc(cond, conds));
    end
));

Class(diagDirsum, fStack, rec(
    range := self >> UnifyTypes(List(self.children(), x->x.range()))
));

# Function for Pease twiddle access. Maps n->n to index into
#   Diag(TC(n,2,0)) based upon iteration l.
#   fAccPease(n, r, l)
Class(fAccPease, FuncClass, rec(
   def := (n, r, l) -> rec(N:=n, n:=n),
   lambda := self >> let(
       N := self.params[1], k := Ind(N),
       R := self.params[2], l := self.params[3],

       Lambda(k, let(
           bits:=Log2Int(N)-Log2Int(R),
	       rCyclicShift(
               bin_and(
                   k,
                   concat(
                       bin_shl(
                           2^(bits)-1,
                           l*Log2Int(R),
                           bits
                       ),
		               R-1,
                       Log2Int(R)
                   )
               ),
               Log2Int(R),
               Log2Int(N)
           )
	   )))
));

#F fPad(<diag_func>)
#F
#F   Index function with 0 range and non zero domain.
#F   Returns result of <diag_func> diagonal function wrapped into funcExp().
#F   Relies on rewriting nth(<loc>, funcExp(A)) -> A.

Class(fPad, FuncClass, rec(
    def       := (f) -> rec(),

    domain    := self >> self.params[1].domain(),
    range     := self >> 0,

    lambda    := self >> let(
                    i := Ind(self.domain()),
                    l := self.params[1].lambda(),
                    Lambda(i, funcExp(l.at(i)))),

    transpose := self >> Error("fPad is not transposable, needs inert form"),
));

#F fInsert(<map>, <value>)
#F
#F Index function with padding.
#F
#F   Ex: Gath(fInsert([1, 1, 0, 1, 0], 3))*X ==
#F
#F   [ [ 1, 0, 0 ],
#F     [ 0, 1, 0 ],
#F     [ 0, 0, 0 ],  * X + [ 0, 0, 3, 0, 3 ]^T
#F     [ 0, 0, 1 ],
#F     [ 0, 0, 0 ] ]
#F
#F

Class(fInsert, FuncClass, rec(
    def := (map, val) -> rec(),
    domain := self >> Length(self.params[1]),
    range  := self >> Sum(self.params[1]),
    lambda := self >> let(
        i := Ind(self.domain()),
        map := self.params[1],
        v   := funcExp(self.params[2]),
        ctr := Counter(0),
        Lambda(i, ApplyFunc( cond,
            ConcatList([1..Length(map)], j ->[eq(i, j-1), Cond(map[j]<>0, ctr.next(), v)])
            :: [errExp(TInt)]))),
    transpose := self >> Error("fInsert is not transposable, needs inert form"),
));


#############################################################################
##
#F  MergeAndGroupRecord(<dst>,<rec1>,<rec2>...)  . . . . . . . merge the fields of records. Same fields get grouped into a set.
##
MergeAndGroupRecords := function ( arg )
    local   res,        # merged record, result
            record,     # one of the arguments
            name,       # name of one component of <record>
            set_list;  # keeps track if a set was created for a field

    res := When(arg=[], rec(), arg[1]);
    set_list := [];
    for record  in  arg{[2..Length(arg)]} do
        for name  in UserRecFields( record )  do
            if IsBound(res.(name))  then
                if name in set_list then
                    res.(name) := UnionSet(res.(name), Set([record.(name)]) );
                else
                    res.(name) := UnionSet(Set([res.(name)]), Set([record.(name)]));
                    Add(set_list, name);
                fi;
            else
                res.(name) := record.(name);
            fi;
        od;
    od;
    return res;
end;

Class(CudaCodegen, DefaultCodegen, rec(

    SIMTISum := meth(self, o, y, x, opts)
        local lcode, simtidx, decls, d, v;

        simtidx := o.simt_dim.simt_idx();
        lcode := simt_loop(simtidx, o.var, o.domain,
            self(o.child(1), y, x, opts));

        # If not a regular loop, add simt index to inner array's parallel context
        if simtidx <> Ignore then
            self.simtidx_rec.(o.var.id) := simtidx;
            decls := Collect(lcode, @(1, decl, d -> ForAny(d.vars, v -> IsArrayT(v.t))));
            for d in decls do
                DoForAll(d.vars, v -> When(IsBound(v.parctx), Add(v.parctx, [o.var, simtidx]), v.setAttrTo("parctx", [ [o.var, simtidx] ])) );
            od;
        fi;

        return When( ObjId(o.simt_dim)=ASIMTKernelFlag, simt_block( lcode ), lcode );
    end,

    SIMTSUM := meth(self, o,y,x,opts)
        local i, ch, numch, code_ch, cch, pb, parb;
        ch    := o.children();
        numch := Length(ch);
        code_ch := chain();

        for i in [1..numch] do
            cch := self(ch[i], y, x, opts);

            if o.simt_dim.need_cond(i) then
                Add(code_ch.cmds, IF(o.simt_dim.simt_cond(i), cch, skip()) );
            else
                Add(code_ch.cmds, cch);
            fi;

        od;

        return code_ch;
    end,

    BB := meth(self,o,y,x,opts)
        local res;

        res := Inherited(o, y, x, opts);

        # inner temps are unrolled - drop from pre/post tables (See Compose below)
        if opts.mempool then
            self.tarray_pre  := DropLast(self.tarray_pre,  1);
            self.tarray_post := DropLast(self.tarray_post, 1);
            Add(self.tarray_pre,  rec());
            Add(self.tarray_post, rec());
        fi;

        return res;

    end,

    Compose := meth(self, o,y,x,opts)
        local ch, numch, vecs, code_ch, get_simt_sync, ta_pre, ta_post, ci, cch, simts, parb;
        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);

        get_simt_sync := (c) -> let(simtss := List( Collect(c, @.cond(e -> IsRec(e) and IsBound(e.simt_dim))), e -> e.simt_dim.simt_sync() ),
                                   When(simtss<>[], Maximum(simtss), skip() )
                                );

        code_ch := [];

        if opts.mempool then
            ta_pre  := Last(self.tarray_pre);
            ta_post := Last(self.tarray_post);
        fi;

        for ci in [1..numch] do
            if opts.mempool then
                Add(self.tarray_pre,  rec());
                Add(self.tarray_post, rec());
            fi;
            cch := self(ch[ci], vecs[ci+1], vecs[ci], opts);
            # Determine sync primitive at ch's boundary
            simts := get_simt_sync(ch[ci]);
            Append(code_ch, When(ObjId(simts)<>skip, [cch, simts], [cch]));

            if opts.mempool then
                # Add new temps to pre/post tables.
                # Composition creates a pre/post relationship btw temps.
                if Last(self.tarray_pre) = rec() then
                    ta_pre  := MergeAndGroupRecords(ta_pre,  rec( (vecs[ci+1].id) := Set([vecs[ci]]) ) );
                    ta_post := MergeAndGroupRecords(ta_post, rec( (vecs[ci].id)   := Set([vecs[ci+1]]) ) );
                else
                    ta_pre  := MergeAndGroupRecords(ta_pre,  Last(self.tarray_pre) );
                    ta_post := MergeAndGroupRecords(ta_post, Last(self.tarray_post) );
                fi;
                self.tarray_pre  := DropLast(self.tarray_pre,  1);
                self.tarray_post := DropLast(self.tarray_post, 1);

            fi;
        od;

        # Wrap code in variable declaration.
        return decl(
                Difference(Flat(vecs{[2..Length(vecs)-1]}), Flat([x,y])),
                chain( code_ch )
            );
    end,

    wrap_gpu_timing := function(ker_list, opts)
        local start, stop, ms;

        if not opts.gpu_timing then
            return ker_list;
        fi;

        Append(opts.includes, ["<stdio.h>", "<helper_cuda.h>"]);

        start := var.fresh_t("start", T_CudaEvent);
        stop := var.fresh_t("stop", T_CudaEvent);
        ms    := var.fresh_t("ms", T_Real(32));

        return decl([start, stop, ms],
                    chain(
                        cu_event_create(addrof(start)),
                        cu_event_create(addrof(stop)),
                        cu_check_errors(cu_event_record(start, false)),
                        ker_list,
                        cu_check_errors(cu_event_record(stop, false)),
                        cu_check_errors(cu_event_synchronize(stop, false)),
                        assign(ms, V(0)),
                        cu_event_elapsed_time(addrof(ms), start, stop),
                        #cprintf("SPIRAL-based CUDA execution [ms]:\\t%f\\n", ms),
                        cu_event_destroy(start),
                        cu_event_destroy(stop)
                    )
            );
    end,

    wrap_kernel := meth(self, ker_desc, io, params, o_dims, opts)
        local cp_in, cp_out;
        cp_out := cu_memcpy(var.table.("h"::io[1].id), io[1], o_dims[1], cudaMemcpyDeviceToHost);
        cp_in  := [cu_memcpy(io[2], var.table.("h"::io[2].id), o_dims[2], cudaMemcpyHostToDevice)]
                ::List(params, p -> cu_memcpy(p, var.table.("h"::p.id), p.t.size, cudaMemcpyHostToDevice));

        return decl(Set(List(ker_desc.cuda_kers, kc ->kc.dim_grid))::Set(List(ker_desc.cuda_kers, kc ->kc.dim_block)),
                chain(
                    cp_in,
                    self.wrap_gpu_timing(
                        List(ker_desc.cuda_kers, kc -> ApplyFunc(cu_call,[kc.cuda_ker.id, kc.dim_grid, kc.dim_block]::kc.cuda_ker.params) ),
                        opts),
                    cp_out
                ));
    end,

    wrap_kernel_devfunc := meth(self, ker_desc, io, params, o_dims, opts)
        return decl(Set(List(ker_desc.cuda_kers, kc ->kc.dim_grid))::Set(List(ker_desc.cuda_kers, kc ->kc.dim_block)),
                chain(
                        List(ker_desc.cuda_kers, kc -> ApplyFunc(cu_call,[kc.cuda_ker.id, kc.dim_grid, kc.dim_block]::kc.cuda_ker.params) )
                ));
    end,

    get_dim_idx := (self, ker, IdxT) >> let( ll := List(Collect(ker, @(1,simt_loop,l->ObjId(l.simt_idx)=IdxT) ), l->l.simt_idx ),
                                         When(Length(ll)>0,
                                            Checked(ForAll(ll, l -> l.size()=ll[1].size()), ll[1].size()),
                                            V(1)
                                        )),

    _reduce_tarray_graph := function(pre, post, ta_list)
        local ta_id, ta, t, new_pre, new_post;
        new_pre  := Copy(pre);
        new_post := Copy(post);
        # Only consider provided arrays
        for ta_id in UnionSet(UserRecFields(pre), UserRecFields(post)) do
            ta := var.table.(ta_id);
            if not ta in ta_list then
                if IsBound(new_pre.(ta_id)) then
                    for t in new_pre.(ta_id) do
                        new_post.(t.id) := UnionSet( When(IsBound(new_post.(t.id)),  new_post.(t.id),  []),
                                                     When(IsBound(new_post.(ta_id)), new_post.(ta_id), []) );
                        new_post.(t.id) := Difference(new_post.(t.id), [ta]);
                    od;
                fi;
                if IsBound(new_post.(ta_id)) then
                    for t in new_post.(ta_id) do
                        new_pre.(t.id) := UnionSet( When(IsBound(new_pre.(t.id)),  new_pre.(t.id),  []),
                                                    When(IsBound(new_pre.(ta_id)), new_pre.(ta_id), []) );
                        new_pre.(t.id) := Difference(new_pre.(t.id), [ta]);
                    od;
                fi;

                if IsBound(new_pre.(ta_id)) then
                    Unbind(new_pre.(ta_id));
                fi;
                if IsBound(new_post.(ta_id)) then
                    Unbind(new_post.(ta_id));
                fi;
            fi;
        od;

        return [new_pre, new_post];
    end,

    _next_color := function(i, e, exclude)
        local c;
        c := i;
        while c<=e and c in exclude do
            c := c+1;
        od;

        return c;
    end,

    _simple_coloring := meth(self, pre, post, taken)
        local q, x, y, ta, locked, res, id_set, id_cnt, col, parb, tas_on_altb;

        id_set := UnionSet(UserRecFields(pre), UserRecFields(post));
        id_cnt := Length(id_set);

        x := List(Filtered(id_set, v -> not IsBound(pre.(v))),  vid -> var.table.(vid) );
        y := List(Filtered(id_set, v -> not IsBound(post.(v))), vid -> var.table.(vid) );

        res := Copy(taken);
        locked := Set(List(UserRecFields(taken), v -> taken.(v)));
        q := Filtered( Flat(List(x, v -> post.(v.id))), v -> not v in y and not IsBound(taken.(v.id)) );

        while q <> [] do
            [ta, q] := SplitAt(q, 1);
            ta := ta[1];
            col := self._next_color(1, id_cnt, locked
                                                ::List(Filtered(pre.(ta.id), t -> IsBound(res.(t.id)) ), v -> res.(v.id))
                        );
            res.(ta.id) := col;

            #Setup next iteration
            if IsBound(post.(ta.id)) and ForAll(Flat( List(post.(ta.id), p -> pre.(p.id) ) ),
                                            v -> IsBound(res.(v.id))) then
                    # push on queue
                    q := Filtered(post.(ta.id), p -> not p in y)::q;
            fi;
            # else do nothing - still need to complete other par. branches

            if Length(pre.(ta.id)) > 1 then
                #Unlock color of previous divergent point
                locked := DropLast(locked, 1);
            fi;
            if IsBound(post.(ta.id)) and Length(post.(ta.id)) > 1 then
                Add(locked, col);
            fi;

        od;

        return res;
    end,

    _map_col2mem := function(coloring, decl_specs)
        local c, p, ta, cols, samecol_tas, ta2mem, mem_list;

        cols := Set(Flat(List(UserRecFields(coloring), v -> coloring.(v))));
        ta2mem := rec();
        mem_list := [];
        for c in cols do
            samecol_tas := List(Filtered(UserRecFields(coloring), v -> c = coloring.(v)), vid -> var.table.(vid));
            p := var.fresh_t("P", TArray(samecol_tas[1].t.t, Maximum(List(samecol_tas, v -> v.t.size))) );
            p.("decl_specs") := decl_specs;
            for ta in samecol_tas do
                ta2mem.(ta.id) := p;
            od;
            Add(mem_list, p);
        od;

        return [ta2mem, mem_list];
    end,

    compact_arrays := meth(self, ker, ta_list, ker_args, decl_specs, opts)
        local cross_ker_tas, pre, post, ck_pre, ck_post, ck_col, coloring, ta2mem;

        #Currently limited to coloring cross-kernel tas
        cross_ker_tas := Filtered(ta_list, v -> IsBound(v.crossker) and v.crossker);
        ta_list := Difference(ta_list, cross_ker_tas);
        [ck_pre, ck_post] := self._reduce_tarray_graph(self.tarray_pre, self.tarray_post, cross_ker_tas::ker_args);
        ck_col := self._simple_coloring(ck_pre, ck_post, rec());
        #assign mem areas based on coloring
        [ta2mem, cross_ker_tas] := self._map_col2mem(ck_col, decl_specs);
        ker := SubstVars(ker, ta2mem);

        return [ker, cross_ker_tas::ta_list];
    end,

    extract_tarrays := meth(self, ker, cond, opts)
        local decls, d, ta, tarrays;

        decls := Collect(ker, @(1, decl, d -> ForAny(d.vars, v -> IsArrayT(v.t)) ) );

        tarrays := [];
        for d in decls do
            # Collect temp arrays based on cond
            ta := Filtered(d.vars, v -> cond(v) );
            d.vars := Difference(d.vars, ta);

            Append(tarrays, ta);
        od;

        return tarrays;
    end,

    to_grid_arrays := meth(self, ker, ta_list, ker_args, opts)
        local ta, idx_list, blk_ids, th_ids, idx, idx_cnt, idx_rng, derefs, nths, v;

        for ta in ta_list do
            ta.("decl_specs") := ["__device__"];
            idx_list := When(IsBound(ta.parctx), ta.parctx, []);
            #parctx format: [ [loopvar1, simtIdx1], ... ]
            blk_ids := Filtered(idx_list, idx -> IsSimtBlockIdx(idx[2])); Sort(blk_ids, (i1, i2) -> i1[2].name < i2[2].name);
            th_ids  := Filtered(idx_list, idx -> IsSimtThreadIdx(idx[2])); Sort(th_ids, (i1, i2) -> i1[2].name < i2[2].name);
            for idx in th_ids::blk_ids do
                idx_cnt := idx[2].count(); # Num of threads associated with index
                if idx_cnt > 1 then
                    idx_rng := idx[2].get_rng(); # Thread range: [<1st tid>, <last tid>]
                    derefs := Collect(ker, @(1, deref, v -> ta in v.free()));
                    nths   := Collect(ker, @(1,   nth, v -> ta in v.free()));

                    for v in derefs do
                        v.loc := add(v.loc, ta.t.size*(idx[2]-idx_rng[1]) );
                    od;
                    for v in nths do
                        v.idx := add(v.idx, ta.t.size*(idx[2]-idx_rng[1]));
                    od;
                    ta.t.size := ta.t.size*idx_cnt;
                fi;
            od;
        od;

        if opts.mempool then
            [ker, ta_list] := self.compact_arrays(ker, ta_list, ker_args, ["__device__"], opts);
        fi;

        return [ker, ta_list];
    end,

    to_block_shmem_arrays := meth(self, ker, ta_list, ker_args, opts)
        local ta, idx_list, blk_ids, th_ids, idx, idx_cnt, idx_rng, derefs, nths, size, padded, v;

        for ta in ta_list do
            padded := false;
            ta.("decl_specs") := ["__shared__"];
            idx_list := When(IsBound(ta.parctx), ta.parctx, []);
            th_ids  := Filtered(idx_list, idx -> IsSimtThreadIdx(idx[2])); Sort(th_ids, (i1, i2) -> i1[2].name < i2[2].name);
            for idx in th_ids do
                idx_cnt := idx[2].count();
                if idx_cnt > 1 then
                    size := ta.t.size;
                    if not padded and Mod(size, 2) = 0 then
                        size := size+1;
                    fi;
                    padded := true;
                    idx_rng := idx[2].get_rng();
                    derefs := Collect(ker, @(1, deref, v -> ta in v.free()));
                    nths   := Collect(ker, @(1,   nth, v -> ta in v.free()));

                    for v in derefs do
                        v.loc := add(v.loc, size*(idx[2]-idx_rng[1]) );
                    od;
                    for v in nths do
                        v.idx := add(v.idx, size*(idx[2]-idx_rng[1]));
                    od;
                    ta.t.size := size*idx_cnt;
                fi;
            od;
        od;

        ker := decl(ta_list, ker);

        return [ker, ta_list];
    end,

    extract_ker_datas := meth(self, cuda_icode, opts)
        local ker_datas, tcuicode, d;

        ker_datas := List(Collect(cuda_icode, data), d -> d.var);

        tcuicode := SubstTopDown(cuda_icode, data, e -> e.cmd);
        while tcuicode <> cuda_icode do
            cuda_icode := tcuicode;
            tcuicode := SubstTopDown(cuda_icode, data, e -> e.cmd);
        od;

        return [cuda_icode, ker_datas];
    end,

    _get_data_loopindex_deps := function(icode, d, opts)
        local ln, fv, la, li, lv, locli;
        ln := Collect(icode, @(1, nth).cond(v -> d in v.loc.free()));
        fv := Set( Flat(List(ln, v -> v.idx.free())) );
        li := Filtered(fv, v -> IsLoopIndex(v));

        #If code in SSA form using lv set to avoid cyclic deps could be avoided
        fv := Difference(fv, li); lv := Copy(fv);
        while(fv <> []) do
            la := Collect(icode, @(1, assign).cond(a -> a.loc in fv));
            fv := Set( Flat(List(la, a -> a.exp.free())) );
            locli := Filtered(fv, v -> IsLoopIndex(v));
            fv := Difference(fv, locli);
            fv := Difference(fv, lv);
            li := UnionSet(li, locli);
            lv := UnionSet(lv, fv);
        od;

        return li;
    end,

    ker_datas_to_device := meth(self, ker_datas, cuda_icode, opts)
        local d, li;

        for d in Reversed(ker_datas) do
            li := self._get_data_loopindex_deps(cuda_icode, d, opts);
            # If any warp-divergent access then allocate in dev mem otherwise constant.
            if ForAny(li, v -> IsBound(self.simtidx_rec.(v.id))
                                and ObjId(self.simtidx_rec.(v.id)) = simtThreadIdxX) then
                d.("decl_specs") := ["__device__"];
            else
                d.("decl_specs") := ["__constant__"];
            fi;
            cuda_icode := data(d, d.value, cuda_icode);
        od;

        return cuda_icode;
    end,

    make_kernels := meth(self, full_kernel, device_data, full_args, opts)
        local kercx, ker_cnt, ker, ker_args, ker_datas, cuda_ker, cuda_desc, cuda_icode,
                dim_grid, dim_block, tbody, tas_grid, tas_block_shmem, cross_ker_tas, ta;

        cuda_desc := rec(grid_tarrays := [], cuda_kers := [] );

        [full_kernel, ker_datas] := self.extract_ker_datas(full_kernel, opts);

        # Mark cross kernel temporaries
        cross_ker_tas := Filtered(Flat(List(Collect(full_kernel, @@(1, decl, (d, ctx) -> not IsBound(ctx.simt_block) or ctx.simt_block = [] )),
                            d -> d.vars)), v -> IsArrayT(v.t));
        DoForAll(cross_ker_tas, v -> v.setAttrTo("crossker", true));
        #Extract all temp arrays that should map to dev mem
        tas_grid := self.extract_tarrays(full_kernel,
                                            When(opts.use_shmem,
                                                    v -> (IsBound(v.crossker) and v.crossker) or (IsBound(v.t.size) and (v.t.size >= opts.max_shmem_size/8/2)),
                                                    True
                                            ), opts);
        #Allocate in dev mem
        [full_kernel, tas_grid] := self.to_grid_arrays(full_kernel, tas_grid, full_args, opts);
        Append(cuda_desc.grid_tarrays, tas_grid);

        kercx := Collect(full_kernel, simt_block);
        kercx := When(kercx = [], [ full_kernel ], kercx);
        ker_cnt := 0;
        for ker in kercx do

            tbody := When(ObjId(ker) = simt_block, chain(ker.cmds), ker);
            ker_args := Intersection(device_data, tbody.free())::Intersection(full_args, tbody.free());
            #Extract remaining shared mem temp arrays if required
            tas_block_shmem := self.extract_tarrays(tbody, v -> opts.use_shmem, opts);
            #Allocate in shared mem
            [tbody, tas_block_shmem] := self.to_block_shmem_arrays(tbody, tas_block_shmem, ker_args, opts);

            dim_grid := var.fresh_t_obj("g", Dim3(), rec( x := self.get_dim_idx(tbody, simtBlockIdxX),
                                                          y := self.get_dim_idx(tbody, simtBlockIdxY),
                                                          z := self.get_dim_idx(tbody, simtBlockIdxZ)
                                                        ) );
            dim_block := var.fresh_t_obj("b", Dim3(), rec( x := self.get_dim_idx(tbody, simtThreadIdxX),
                                                           y := self.get_dim_idx(tbody, simtThreadIdxY),
                                                           z := self.get_dim_idx(tbody, simtThreadIdxZ)
                                                        ) );

            cuda_ker := specifiers_func(["__global__"], TVoid, "ker_code"::StringInt(ker_cnt), Filtered(ker_args, d -> not IsBound(d.("decl_specs")) or not "__constant__" in d.("decl_specs")), tbody );

            Add(cuda_desc.cuda_kers,  rec(dim_grid := dim_grid, dim_block := dim_block, cuda_ker := cuda_ker));

            ker_cnt := ker_cnt + 1;
        od;

        cuda_icode := chain(List(cuda_desc.cuda_kers, ck -> ck.cuda_ker));
        cuda_icode := self.ker_datas_to_device(ker_datas, cuda_icode, opts);

        return [cuda_desc, cuda_icode];

    end,

    make_init := meth(self, initname, initparams, init_datas, device_datas, cuda_desc, io, params, o_dims, opts)
        local d, dd, t_id, t_var, tarrays, init_chain, init_func;

        # Use the generated init for the host arrays
        for d in init_datas do
            t_var := d.var;
            d.var := t_var.clone();
            t_id := d.var.id;
            d.var.id := "h"::t_var.id;
            d.init := SubstVars(d.init, rec((t_var.id) := d.var));
            Unbind(var.table.(t_id));
            var.table.(d.var.id) := d.var;
        od;

        init_chain := chain(List(device_datas, dd ->    When(IsBound(dd.("decl_specs")) and "__constant__" in dd.("decl_specs"),
                                                            chain(SReduce(dd.init, opts),
                                                                cu_memcpy_to_sym(dd, var.table.("h"::dd.id), dd.t.size)
                                                                ),
                                                            chain(cu_allocate(dd, dd.t.t, dd.t.size),
                                                                SReduce(dd.init, opts),
                                                                cu_memcpy(dd, var.table.("h"::dd.id), dd.t.size, cudaMemcpyHostToDevice)
                                                                )
                                                        )
                                )
                                ::When(not (IsBound(opts.devFunc) and opts.devFunc), List(params, dd -> cu_allocate(dd, dd.t.t, dd.t.size) ), [])
                            );
        if not (IsBound(opts.devFunc) and opts.devFunc) then
            for i in [1..Length(io)] do
                Add(init_chain.cmds, cu_allocate(io[i], io[i].t.t, o_dims[i]));
            od;
        fi;

        init_func := func(TVoid, initname, Set(Collect(init_chain, param)), init_chain);
        return init_func;
    end,

    make_destroy := meth(self, destroyname, init_datas, device_datas, cuda_desc, io, params, opts)
        local destroy_chain, destroy_func;

        destroy_chain := chain( List(Filtered(device_datas, dd -> not IsBound(dd.("decl_specs")) or not "__constant__" in dd.("decl_specs") ),
                                    d -> cu_free(d))
                                ::When(not (IsBound(opts.devFunc) and opts.devFunc), List(params, d -> cu_free(d)), [])
                                ::When(IsBound(opts.devFunc) and opts.devFunc, [], List(io, d -> cu_free(d))));

        destroy_func := func(TVoid, destroyname, Set(Collect(destroy_chain, param)), destroy_chain);
        return destroy_func;
    end,

    assemble_prog := meth(self, datas, initparams, params, io, o, icode, opts)
        local hparams, hio, p, hp, init_datas, device_datas, ker_args,
              sub, cuda_sub, initsub, destroysub,
              cuda_desc, cuda_icode, icode_wrap, initcode, destroycode, prog,
              wrapk_f;

        wrapk_f := When(IsBound(opts.devFunc) and opts.devFunc, self.wrap_kernel_devfunc, self.wrap_kernel);
        hparams := [];
        for p in params do
            p.setAttr("decl_as_ptr");
            hp := p.clone();
            Unbind(var.table.(hp.id));
            hp.id := When(IsBound(opts.devFunc) and opts.devFunc, p.id, "h"::p.id);
            var.table.(hp.id) := hp;
            Add(hparams, hp);
        od;

        hio := [];

        if not (IsBound(opts.devFunc) and opts.devFunc) then
            for p in io do
                hp := p.clone();
                Unbind(var.table.(hp.id));
                hp.id := "h"::p.id;
                var.table.(hp.id) := hp;
                Add(hio, hp);
            od;
        fi;

        # Device data here refers to data initialized in init function and therefor residing in dev mem
        init_datas := Filtered(datas, e -> IsBound(e.var.init));
        device_datas := List(init_datas, d -> d.var);

        sub := Cond(IsBound(opts.subName), opts.subName, "transform");
        cuda_sub := "transform_ker";
        initsub := Cond(IsBound(opts.subName), Concat("init_", opts.subName), "init");
        destroysub := Cond(IsBound(opts.subName), Concat("destroy_", opts.subName), "destroy");
        ker_args := Concatenation(io, params);

        [cuda_desc, cuda_icode] := self.make_kernels(icode, device_datas, ker_args, opts);

        icode_wrap := func(TVoid, sub, Concatenation(When(IsBound(opts.devFunc) and opts.devFunc, io, hio), hparams), chain(wrapk_f(self, cuda_desc, io, params, o.dimensions, opts)));
        if IsBound(opts.generateInitFunc) and opts.generateInitFunc then
            initcode := self.make_init(initsub, initparams, init_datas, device_datas, cuda_desc, io, params, o.dimensions, opts);
            destroycode := self.make_destroy(destroysub, init_datas, device_datas, cuda_desc, io, params, opts);
            prog := program(
                decl(List(datas, x->x.var)::cuda_desc.grid_tarrays::device_datas::params::When(IsBound(opts.devFunc) and opts.devFunc, [], io),
                    chain(
                        initcode,
                        cuda_icode,
                        icode_wrap,
                        destroycode
                    )));
        else
            initcode := self.make_init(initsub, initparams, init_datas, device_datas, cuda_desc, io, params, o.dimensions, opts);
            destroycode := self.make_destroy(destroysub, init_datas, device_datas, cuda_desc, io, params, opts);
            prog := program(
                decl(cuda_desc.grid_tarrays::params::When(IsBound(opts.devFunc) and opts.devFunc, [], io),
                    chain(
                        initcode,
                        cuda_icode,
                        icode_wrap,
                        destroycode
                    )));
        fi;

        return prog;
    end,

    Formula := meth(self, o, y, x, opts)
        local icode, icode1, datas, prog, initparams, params, io, t, o, field;

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

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

        io := When(x=y, [x], [y, x]);
        io := io::When(IsBound(opts.Xptr), [opts.Xptr], []);
        io := io::When(IsBound(opts.Yptr), [opts.Yptr], []);

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

        if opts.mempool then
            self.tarray_pre  := [rec()];
            self.tarray_post := [rec()];
        fi;
        self.simtidx_rec := rec();

        datas := Collect(o, FDataOfs);
        [o,t] := UTimedAction(BlockSumsOpts(o, opts)); #PrintLine("BlockSums ", t);
        [icode,t] := UTimedAction(self(o, y, x, opts)); #PrintLine("codegen ", t);

        if opts.mempool then
            self.tarray_pre := self.tarray_pre[1];
            self.tarray_post := self.tarray_post[1];
            for field in Difference(UserRecFields(self.tarray_pre), List(io, v -> v.id)) do
                self.tarray_pre.(field)  := Set(Flat(self.tarray_pre.(field)));
                self.tarray_post.(field) := Set(Flat(self.tarray_post.(field)));
            od;
            for field in List(io, v -> v.id) do
                if IsBound(self.tarray_pre.(field)) then
                    self.tarray_pre.(field)  := Set(Flat(self.tarray_pre.(field)));
                fi;
                if IsBound(self.tarray_post.(field)) then
                    self.tarray_post.(field) := Set(Flat(self.tarray_post.(field)));
                fi;
            od;

        fi;

        icode := RemoveAssignAcc(icode);
        Unbind(Compile.times);
        [icode,t] := UTimedAction(BlockUnroll(icode, opts)); #PrintLine("BlockUnroll ", t);
        icode := DeclareHidden(icode);
        if IsBound(opts.isFixedPoint) and opts.isFixedPoint then
            icode := FixedPointCode(icode, opts.bits, opts.fracbits);
        fi;

        if IsBound(opts.postCompileStrategy) then
            icode := opts.postCompileStrategy(icode, opts);
        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;

        prog := self.assemble_prog(datas, initparams, params, io, o, icode, opts);
        prog.dimensions := o.dims();
        return prog;
    end,
    ));


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

@TVectSVE := @.cond(x->x.t=TVectSVE);
@ValueTVectSVE := @.cond(x->x.t=TVectSVE and ObjId(x)=Value);

Class(SVEUnparserMixin, rec(
    sve_loopn := (self, o, i, is) >> let(v := o.var.id,
        pg := self.opts.pg.id,
        lo := 0,
        hi := o.range,
        Print(Blanks(i), "int ", v, " = 0;\n",
            Blanks(i), pg, " = svwhilelt_b64(", v, ", ", hi, ");\n",
            Blanks(i), "do {\n",
            self(o.cmd, i + is, is),
            Blanks(i + is), v, " += svcntd();\n",
            Blanks(i + is), pg, " = svwhilelt_b64(", v, ", ", hi, ");\n",
            Blanks(i), "} while(svptest_any(svptrue_b64(), ", self.opts.pg.id, "));\n")
    ),

# svfloat64_t svmulx[_n_f64]_z(svbool_t pg, svfloat64_t op1, float64_t op2)

# svcntd()
    sve_svcntd := (self, o, i, is) >> Print("svcntd()"),

# svfloat64_t
    TVectSVE := (self, t, vars, i, is) >> Print(When(IsBound(self.opts.TRealSVEtype), self.opts.TRealSVEtype, "svfloat64_t"), " ", self.infix(vars, ", ", i + is)),

# svbool_t
    TBoolSVE := (self, t, vars, i, is) >> Print("svbool_t ", self.infix(vars, ", ", i + is)),

# int64_t
    TInt64SVE := (self, t, vars, i, is) >> Print("int64t ", self.infix(vars, ", ", i + is)),

# svfloat64x2_t
    TVect := (self, t, vars, i, is) >> Print(When(IsBound(self.opts.TRealSVEtype), self.opts.TRealSVEtype, "svfloat64x2_t"), " ", self.infix(vars, ", ", i + is)),

    velem := (self, o, i, is) >> Print(self(o.loc, i, is), ".v", StringInt(o.idx.v)),

## svfloat64_t svld1_gather_[u64]offset[_f64](svbool_t pg, const float64_t *base, svuint64_t offsets)
    sve_gath := (self, o, i, is) >> Print("svld1_gather_u64offset_f64(", self(o.pg, i, is), ", ", self(o.loc, i, is), ", ",
        "svindex_u64(0, (int64_t)(", self(o.stride, i, is), " * sizeof(float64_t)))",
        ")"),

# svint64_t incx_vec = svindex_s64(0, incx);
# svfloat64_t dx_vec = svld1_gather_index(pg, dx, incx_vec);
#    sve_gath := (self, o, i, is) >> Print("svld1_gather_index(", self(o.pg, i, is), ", ", self(o.loc, i, is), ", ",
#        "svindex_s64(0, (int64_t)", self(o.stride, i, is), ")",
#        ")"),

## void svst1_scatter_u64_offset_f64(svbool_t pg, svuint64_t bases, int64_t offset, svfloat64_t data)
    sve_scat := (self, o, i, is) >> Print(Blanks(i), "svst1_scatter_u64offset_f64(", self(o.pg, i, is), ", ((float64_t  *)", self(o.loc, i, is), "), ",
            "svindex_u64(0, (int64_t)(", self(o.stride, i, is), " * sizeof(float64_t)))", ", ", self(o.exp, i, is), ");\n"),

# svfloat64x2_t svld2[_f64](svbool_t pg, const float64_t *base)
    sve_ld2 := (self, o, i, is) >> Print("svld2_f64(", self(o.pg, i, is), ", ", self(o.loc, i, is), ")"),

# void svst2[_f64](svbool_t pg, float64_t *base, svfloat64x2_t data)
    sve_st2 := (self, o, i, is) >> Print(Blanks(i), "svst2_f64(", self(o.pg, i, is), ", ", self(o.loc, i, is), ", ", self(o.exp, i, is), ");\n"),

# Arithmetic
    mul := (self, o, i, is) >> let(n := Length(o.args), Cond(
        not o.t = TVectSVE,
            Print("(", self.pinfix(o.args, ")*("), ")"),
 	    n > 2 and n mod 2 <> 0,
            self(mul(o.args[1], ApplyFunc(mul, Drop(o.args, 1))), i, is),
        n > 2,
            self(mul(ApplyFunc(mul, o.args{[1..n/2]}), ApplyFunc(mul, o.args{[n/2+1..n]})), i, is),
        CondPat(o,
# svfloat64_t svmul[_n_f64]_x(svbool_t pg, svfloat64_t op1, float64_t op2)
           [mul, @ValueTVectSVE, @TVectSVE],
               self.printf("svmul_n_f64_x($1, $2, $3)", [self.opts.pg, o.args[2], o.args[1]]),
           [mul, @TVectSVE, @ValueTVectSVE],
               self.printf("svmul_n_f64_x($1, $2, $3)", [self.opts.pg, o.args[1], o.args[2]]),
           [mul, @TReal, @TVectSVE],
               self.printf("svmul_n_f64_x($1, $2, $3)", [self.opts.pg, o.args[2], o.args[1]]),
           [mul, @TVectSVE, @TReal],
               self.printf("svmul_n_f64_x($1, $2, $3)", [self.opts.pg, o.args[1], o.args[2]]),
           [mul, @TInt, @TVectSVE],
               self(mul(_toReal(o.args[1]), o.args[2]), i, is),
           [mul, @TVectSVE, @TInt],
               self(mul(o.args[1], _toReal(o.args[2])), i, is),
# svfloat64_t svmul[_f64]_x(svbool_t pg, svfloat64_t op1, float64_t op2)
           [mul, @TVectSVE, @TVectSVE],
               self.printf("svmul_f64_x($1, $2, $3)", [self.opts.pg, o.args[1], o.args[2]]),
           Error("Don't know how to unparse <o>. Unrecognized type combination")))
    ),

# -- add --
    add := (self, o, i, is) >> let(n := Length(o.args), Cond(
        not o.t = TVectSVE,
            self.pinfix(o.args, " + "),
        n > 2 and n mod 2 <> 0,
            self(add(o.args[1], ApplyFunc(add, Drop(o.args, 1))), i, is),
        n > 2,
            self(add(ApplyFunc(add, o.args{[1..n/2]}), ApplyFunc(add, o.args{[n/2+1..n]})), i, is),
        CondPat(o,
            [add, @TReal, @TVectSVE],
# svfloat64_t svadd[_n_f64]_x(svbool_t pg, svfloat64_t op1, svfloat64_t op2)
                self.printf("svadd_n_f64_x($1, $2, $3)", [self.opts.pg, o.args[2], o.args[1]]),
            [add, @TVectSVE, @TReal],
                self.printf("svadd_n_f64_x($1, $2, $3)", [self.opts.pg, o.args[1], o.args[2]]),
            [add, @TInt, @TVectSVE],
                self(add(_toReal(o.args[1]), o.args[2]), i, is),
            [add, @TVectSVE, @TInt],
                self(add(o.args[1], _toReal(o.args[2])), i, is),
# svfloat64_t svadd[_f64]_x(svbool_t pg, svfloat64_t op1, svfloat64_t op2)
            [add, @TVectSVE, @TVectSVE],
                self.printf("svadd_f64_x($1, $2, $3)", [self.opts.pg, o.args[1], o.args[2]]),
            Error("Don't know how to unparse <o>. Unrecognized type combination")))
    ),

    sub := (self, o, i, is) >> Cond(
        not o.t = TVectSVE,
            Inherited(o, i, is),
        CondPat(o,
            [sub, @TReal, @TVectSVE],
# svfloat64_t svsub[_n_f64]_x(svbool_t pg, svfloat64_t op1, float64_t op2)
                self.printf("svsubr_n_f64_x($1, $2, $3)", [self.opts.pg, o.args[2], o.args[1]]),
            [sub, @TVectSVE, @TReal],
                self.printf("svsub_n_f64_x($1, $2, $3)", [self.opts.pg, o.args[1], o.args[2]]),
            [sub, @TInt, @TVectSVE],
                self(add(_toReal(o.args[1]), o.args[2]), i, is),
            [sub, @TVectSVE, @TInt],
                self(add(o.args[1], _toReal(o.args[2])), i, is),
# svfloat64_t svadd[_f64]_x(svbool_t pg, svfloat64_t op1, svfloat64_t op2)
            [sub, @TVectSVE, @TVectSVE],
                self.printf("svsub_f64_x($1, $2, $3)", [self.opts.pg, o.args[1], o.args[2]]),
            Error("Don't know how to unparse <o>. Unrecognized type combination"))
    ),

#    neg := (self, o, i, is) >> Cond( _avxT(o.t, self.opts),
#        self(mul(neg(o.t.one()), o.args[1]), i, is),
#        Inherited(o, i, is)),
#

    fma := (self, o, i, is) >> When(
        Length(o.args) <> 3, Error("fma is strictly ternary"),
        CondPat(o,
# svfloat64_t svmla[_n_f64]_x(svbool_t pg, svfloat64_t op1, svfloat64_t op2, float64_t op3)
           [fma, @TVectSVE, @TVectSVE, @ValueTVectSVE],
                self.printf("svmla_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[2], o.args[3]]),
           [fma, @TVectSVE, @ValueTVectSVE, @TVectSVE],
                self.printf("svmla_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[3], o.args[2]]),
           [fma, @TVectSVE, @TVectSVE, @TReal],
                self.printf("svmla_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[2], o.args[3]]),
           [fma, @TVectSVE, @TReal, @TVectSVE],
                self.printf("svmla_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[3], o.args[2]]),
# svfloat64_t svmla[_f64]_x(svbool_t pg, svfloat64_t op1, svfloat64_t op2, svfloat64_t op3)
           [fma, @TVectSVE, @TVectSVE, @TVectSVE],
                self.printf("svmla_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[2], o.args[3]]),
           Error("Don't know how to unparse <o>. Unrecognized type combination")
        )),

    nfma := (self, o, i, is) >> When(
        Length(o.args) <> 3, Error("nfma is strictly ternary"),
        CondPat(o,
# svfloat64_t svnmls[_n_f64]_x(svbool_t pg, svfloat64_t op1, svfloat64_t op2, float64_t op3)
           [nfma, @TVectSVE, @TVectSVE, @ValueTVectSVE],
                self.printf("svnmls_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[2], o.args[3]]),
           [nfma, @TVectSVE, @ValueTVectSVE, @TVectSVE],
                self.printf("svnmls_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[3], o.args[2]]),
           [nfma, @TVectSVE, @TVectSVE, @TReal],
                self.printf("svnmls_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[2], o.args[3]]),
           [nfma, @TVectSVE, @TReal, @TVectSVE],
                self.printf("svnmls_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[3], o.args[2]]),
# svfloat64_t svnmls[_f64]_x(svbool_t pg, svfloat64_t op1, svfloat64_t op2, svfloat64_t op3)
           [nfma, @TVectSVE, @TVectSVE, @TVectSVE],
                self.printf("svnmls_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[2], o.args[3]]),
           Error("Don't know how to unparse <o>. Unrecognized type combination")
        )),

    fms := (self, o, i, is) >> When(
        Length(o.args) <> 3, Error("fms is strictly ternary"),
        CondPat(o,
# svfloat64_t svmls[_n_f64]_x(svbool_t pg, svfloat64_t op1, svfloat64_t op2, float64_t op3)
           [fms, @TVectSVE, @TVectSVE, @ValueTVectSVE],
                self.printf("svmls_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[2], o.args[3]]),
           [fms, @TVectSVE, @ValueTVectSVE, @TVectSVE],
                self.printf("svmls_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[3], o.args[2]]),
           [fms, @TVectSVE, @TVectSVE, @TReal],
                self.printf("svmls_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[2], o.args[3]]),
           [fms, @TVectSVE, @TReal, @TVectSVE],
                self.printf("svmls_n_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[3], o.args[2]]),
# svfloat64_t svmls[_f64]_m(svbool_t pg, svfloat64_t op1, svfloat64_t op2, svfloat64_t op3)
           [fms, @TVectSVE, @TVectSVE, @TVectSVE],
                self.printf("svmls_f64_x($1, $2, $3, $4)", [self.opts.pg, o.args[1], o.args[2], o.args[3]]),
           Error("Don't know how to unparse <o>. Unrecognized type combination")
        )),

));

Class(SVEUnparser, SVEUnparserMixin, CUnparser);


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

CompareCodeMat := (sums, opts) ->
    let(me := Try(CMatrix(CodeSums(Copy(sums), opts), opts)),
        them := MatSPL(sums),
        When(me[1] = false, false,
             InfinityNormMat(me[2]-them) < 1e-5));

_RecursiveFindBug := function(ind, spl, opts, verify_func)
    local c, ch, res, bugspl, origspl, seg, i, len;
    Print(Blanks(ind), ObjId(spl), " - ");
    res := verify_func(spl, opts);
    Print(res, "\n");

    # no bug, verification succeeded
    if res = true then return true;

    # bug, verification failed, now we will recurse and verify the children
    else
        origspl := spl;
        # loops need to be unrolled to get rid of index variables
        if IsBound(spl.unroll) then spl := spl.unroll(); fi;
        for c in spl.children() do
            bugspl := _RecursiveFindBug(ind+3, c, opts, verify_func);
            if bugspl <> true then return bugspl; fi; # bug is in the child
        od;

        # verification succeeded for all of the children, but we will still try to
        # narrow down the problem if the current node is Compose
        if ObjId(spl)=Compose and spl.numChildren() > 2 then
            ind := ind+3;
            PrintLine(Blanks(ind), "Problematic multifactor Compose. Narrowing down the problem");
            ch := spl.children();
            while Length(ch) > 2 do
                len := Length(ch)-1;
                i:=1;
                res := true;
                # the loop terminates when res = false (verif failed) OR everything succeeded (ie no smaller bug)
                while res and i <= Length(ch)-len+1 do
                   seg := ch{[i..i+len-1]};
                   res := verify_func(Compose(seg), opts);
                   PrintLine(Blanks(ind), "len=", len, " i=", i, " ", res);
                   i := i+1;
                od;
                if res=true then return Compose(ch);
                else ch := seg;
                fi;
            od;
            return Compose(ch);
        else
            return origspl; # bug is here and not in the children, and we could not narrow it down further
        fi;
    fi;
end;

# verify_func(spl, opts)
#
RecursiveFindBug := (spl, opts, verify_func) -> _RecursiveFindBug(0, spl, opts, verify_func);

_RecursiveFindBugExp := function(ind, spl, opts, verify_func)
    local c, ch, res, bugspl, origspl, seg, i, len;
    Print(Blanks(ind), ObjId(spl), " - ");
    res := verify_func(spl, opts);
    Print(res, "\n");

    # no bug, verification succeeded
    if res = true then return true;

    # bug, verification failed, now we will recurse and verify the children
    else
        origspl := spl;
        # loops need to be unrolled to get rid of index variables
        if IsBound(spl.unroll) then spl := spl.unroll(); fi;
        for c in spiral.rewrite._children(spl) do
            bugspl := _RecursiveFindBugExp(ind+3, c, opts, verify_func);
            if bugspl <> true then return bugspl; fi; # bug is in the child
        od;

        return origspl; # bug is here and not in the children
    fi;
end;

RecursiveFindBugExp := (spl, opts, verify_func) -> _RecursiveFindBugExp(0, spl, opts, verify_func);

RecursiveFindBugRT := function (rt, opts)
    local chk, s;

    chk := (spl, opts) -> InfinityNormMat(CMatrix(CodeSumsOpts(spl, opts), opts) - MatSPL(spl)) < 1e-4;
    s := SumsRuleTreeOpts(rt, opts);

    return RecursiveFindBug(s, opts, chk);
end;


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

DimLength := (l) -> When(IsList(l), Length(l), 1);

Class(CrossBase, BaseOperation, rec(
  abbrevs := [ arg -> [Flat(arg)] ],

   new := meth(self, L)
        if Length(L)=1 then return L[1]; fi;
        return SPL(WithBases(self, rec(_children:=L, dimensions := TransposedMat(List(L, e -> e.dims())))));
   end,

   area := self >> Sum(self.children(), x->x.area()),

   normalizedArithCost := self >> Sum(self.children(), x -> x.normalizedArithCost()),

));

Declare(Cross);

Class(Cross, CrossBase, rec(
   codeletName:="C",

   isPermutation := self >> false,

   dmn:=self>>let(li:=[],Flat(List(self._children, x-> x.dmn()))),

   rng:=self>>let(li:=[],Flat(List(self._children, x-> x.rng()))),

   sums:= self>>ObjId(self)(List(self.rChildren(),x->x.sums())),

   transpose := self >> self,

   isIdentity := self >> ForAll(self._children, IsIdentitySPL),

   isBlockTransitive := true,
));

Declare(fCross);
Class(fCross, FuncClassOper, rec(
   _perm := true,

   isFunction := true,
   skipOneChild := true,

   range := (self) >> StripList(List(self.rChildren(), (x) -> x.range())),

   advrange := (self) >> List(self.rChildren(), (x) -> x.advrange()[1]),

   domain := (self) >> StripList(List(self.rChildren(), (x) -> x.domain())),

   advdomain := (self) >> List(self.rChildren(), (x) -> x.advdomain()[1]),

   toSpl := (self, allinds, cols) >>
       fCross(List([1..Length(self.rChildren())],
               i -> self.rChildren()[i].toSpl(allinds,cols[i]))),

   downRank := (self, loopid, ind) >>
       fCross(List(self.rChildren(),
           c -> c.downRank(loopid, ind))),

   downRankFull := (self, allinds) >>
       fCross(List(self.rChildren(),
           c -> c.downRankFull(allinds))),

   isIdentity := self >> ForAll(self._children, IsIdentity),

   transpose  := self >> ObjId(self)(List(self._children, e -> e.transpose())),
));

Class(ExplicitGath, Gath);
Class(ExplicitCross, Cross);

Class(ExplicitOLRuleset, RuleSet);
RewriteRules(ExplicitOLRuleset, rec(
   Gath_f2DTrExpR := Rule([Gath, [@(2,fCompose), @(3,f2DTrExplicitR), ...]],
      e-> Gath(fCompose(Drop(@(2).val.rChildren(),1)))*ExplicitGath(ApplyFunc(f2DTr,@(3).val.rChildren()))),

   Gath_f2DTrExpLR := Rule([Gath, [@(2,fCompose), @(3,f2DTrExplicitL), @(4,f2DTrExplicitR), ...]],
      e-> Gath(fCompose([ApplyFunc(f2DTr,@(4).val.rChildren())]::Drop(@(2).val.rChildren(),2)))
      *ExplicitGath(ApplyFunc(f2DTr,@(3).val.rChildren()))),

   Cross_Compose_ExplicitGath_1 := Rule([Cross, [@(1,Compose), ..., @(2,ExplicitGath)], @(3) ],
       e ->
       Compose(Cross(Compose(DropLast(@(1).val.rChildren(),1)), @(3).val),
       ExplicitCross(ApplyFunc(Gath, @(2).val.rChildren()), ApplyFunc(2DI, @(3).val.advdims()[2][1])))),

   Cross_Compose_ExplicitGath_2 := Rule([Cross, @(3), [@(1,Compose), ..., @(2,ExplicitGath)] ],
       e ->
       Compose(Cross(@(3).val, Compose(DropLast(@(1).val.rChildren(),1))),
       ExplicitCross(ApplyFunc(2DI, @(3).val.advdims()[2][1]), ApplyFunc(Gath, @(2).val.rChildren())))),
));

Class(FinalOLRuleset, RuleSet);
RewriteRules(FinalOLRuleset, rec(
   ExplicitCross := Rule(@(1,ExplicitCross),
       e -> ApplyFunc(Cross, @(1).val.rChildren()))
));


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

Import(fftx.platforms.cuda);
Import(fftx.codegen);
Import(simt);

Class(FFTCUDADeviceOpts, FFTXCUDADeviceOpts, rec(
#    tags := [ASIMTKernelFlag(ASIMTGridDimY), ASIMTBlockDimX, ASIMTLoopDim() ],
#    tags := [ASIMTGridDimX, ASIMTBlockDimX, ASIMTKernelFlag(ASIMTLoopDim()) ],
#    tags := [ASIMTBlockDimZ, ASIMTKernelFlag(ASIMTBlockDimX), ASIMTKernelFlag(ASIMTBlockDimY) ],
    tags := [ASIMTKernelFlag(ASIMTGridDimY), ASIMTGridDimX],

    operations := rec(Print := s -> Print("<FFTX FFT CUDA Device options record>"))
));

fftCUDADeviceOpts := function(arg) # specific to FFT size 100...
    local opts;

    opts := Copy(FFTCUDADeviceOpts);
    opts.breakdownRules.Circulant := [Circulant_PRDFT_FDataNT];
    opts.breakdownRules.PRDFT := List([PRDFT1_Base1, PRDFT1_Base2, CopyFields(PRDFT1_CT,
            rec(allChildren := P ->Filtered(PRDFT1_CT.allChildren(P), i->When(P[1] = 100, Cols(i[1]) = 4, true)))),
        PRDFT_PD], _noT);
    opts.breakdownRules.IPRDFT := List([ IPRDFT1_Base1, IPRDFT1_Base2, IPRDFT1_CT, IPRDFT_PD ], _noT);
    opts.breakdownRules.IPRDFT2 := List([ IPRDFT2_Base1, IPRDFT2_Base2, IPRDFT2_CT ], _noT);
    opts.breakdownRules.PRDFT3 := List([ PRDFT3_Base1, PRDFT3_Base2, PRDFT3_CT ], _noT);
    opts.breakdownRules.DFT := [ DFT_Base,
        CopyFields(DFT_CT, rec(children := nt ->Filtered(DFT_CT.children(nt), i->When(nt.params[1] = 100, Cols(i[1]) = 4, true)))),
        DFT_PD ];

    opts.breakdownRules.TTensorInd := [TTensorInd_SIMT];
    opts.breakdownRules.TL := [L_SIMT];
    opts.breakdownRules.TIterHStack := [TIterHStack_SIMT];
    opts.breakdownRules.TIterVStack := [TIterVStack_SIMT];
    opts.breakdownRules.MDPRDFT := [MDPRDFT_tSPL_RowCol_SIMT];
    opts.breakdownRules.IMDPRDFT := [IMDPRDFT_tSPL_RowCol_SIMT];
    opts.breakdownRules.TRC := [TRC_SIMT];
    opts.breakdownRules.MDDFT := [ MDDFT_Base, CopyFields(MDDFT_tSPL_RowCol, rec(switch := true)), MDDFT_RowCol, MDDFT_tSPL_RowCol_SIMT];
    opts.breakdownRules.TTensor := [ AxI_IxB, IxB_AxI ];
    opts.breakdownRules.TTensorI := [ IxA_SIMT,  AxI_SIMT ];
    opts.breakdownRules.TSparseMat := [CopyFields(TSparseMat_base, rec(max_rows := 1)), TSparseMat_VStack];

    opts.globalUnrolling := 27;
    opts.codegen.GathPtr := fftx.codegen.MultiPtrCodegenMixin.GathPtr;
    opts.codegen.ScatPtr := fftx.codegen.MultiPtrCodegenMixin.ScatPtr;
    opts.codegen.OO := (self, o, y, x, opts) >> skip();

    opts.sumsgen.IterHStack := MultiPtrSumsgenMixin.IterHStack;
    opts.preProcess := t -> ApplyStrategy(t,
                    [ MergedRuleSet(RulesFuncSimp, RulesSums),
                      RulesFFTXPromoteNT,
                      MergedRuleSet(RulesFuncSimp, RulesSums) ],
        BUA, opts);

    opts.postProcessSums := (s, opts) -> let(s1 := ApplyStrategy(s, [ MergedRuleSet(RulesFuncSimp, RulesSums, RulesSIMTFission) ], BUA, opts),
        FixUpCUDASigmaSPL(s1, opts));

#    opts.useDeref := false;
    # for debugging...
#    opts.generateInitFunc := false;
#    opts.codegen := DefaultCodegen;
#    opts.arrayBufModifier := "";
#    opts.arrayDataModifier := "static const";
    opts.max_threads := 1024;

    return opts;
end;

confFFTCUDADevice := rec(
    defaultName := "confFFTCUDADevice",
    defaultOpts := (arg) >> rec(useFFTCUDADevice := true),
    confHandler := fftCUDADeviceOpts
);

fftx.FFTXGlobals.registerConf(confFFTCUDADevice);


#
# usage: Read(Filename(home_dir,"load.gap"));
#

#
# Polynomials, Root system, Parabolic rootsystems
#
Read(Filename(home_dir,"lib/poly.gd"));
Read(Filename(home_dir,"lib/rsys.gd"));
Read(Filename(home_dir,"lib/psys.gd"));

#
#
#
Read(Filename(home_dir,"lib/chvadj.gd"));
Read(Filename(home_dir,"lib/nilchv.gd"));
Read(Filename(home_dir,"lib/unichv.gd"));
Read(Filename(home_dir,"lib/unimod.gd"));

#
#
#
Read(Filename(home_dir,"lib/algU.gd"));
Read(Filename(home_dir,"lib/witt.gd"));
Read(Filename(home_dir,"lib/unialg.gd"));
Read(Filename(home_dir,"lib/unicls.gd"));

#
# IMPLEMENTATION
#
Read(Filename(home_dir,"lib/poly.gi"));
Read(Filename(home_dir,"lib/rsys.gi"));
Read(Filename(home_dir,"lib/psys.gi"));

Read(Filename(home_dir,"lib/chvadj.gi"));
Read(Filename(home_dir,"lib/nilchv.gi"));
Read(Filename(home_dir,"lib/unichv.gi"));
Read(Filename(home_dir,"lib/unimod.gi"));

Read(Filename(home_dir,"lib/algU.gi"));
Read(Filename(home_dir,"lib/witt.gi"));
Read(Filename(home_dir,"lib/unialg.gi"));
Read(Filename(home_dir,"lib/unicls.gi"));


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

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

###############################################################################
##
#M  SelfSimilarSemigroup(<list>, <bind_vars>)
##
InstallMethod(SelfSimilarSemigroup, "for [IsList, IsBool]", [IsList, IsBool],
function (list, bind_vars)
  if not AG_IsCorrectRecurList(list, false) then
    Error("in SelfSimilarSemigroup(IsList):\n",
          "  given list is not a correct list representing automaton\n");
  fi;

  # XXX
  return SemigroupOfSelfSimFamily(SelfSimFamily(list, bind_vars));
end);

###############################################################################
##
#M  SelfSimilarSemigroup(<list>, <names>)
##
InstallMethod(SelfSimilarSemigroup, "for [IsList, IsList]", [IsList, IsList],
function (list, names)
  if not AG_IsCorrectRecurList(list, false) then
    Error("error in SelfSimilarSemigroup(IsList, IsList):\n",
          "  given list is not a correct list representing automaton\n");
  fi;

  # XXX
  return SemigroupOfSelfSimFamily(SelfSimFamily(list, names));
end);

###############################################################################
##
#M  SelfSimilarSemigroup(<list>, <names>, <bind_vars>)
##
InstallMethod(SelfSimilarSemigroup, "for [IsList, IsList, IsBool]",
              [IsList, IsList, IsBool],
function (list, names, bind_vars)
  if not AG_IsCorrectRecurList(list, false) then
    Error("error in SelfSimilarSemigroup(IsList):\n",
          "  given list is not a correct list representing automaton\n");
  fi;

  #XXX
  return SemigroupOfSelfSimFamily(SelfSimFamily(list, names, bind_vars));
end);

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

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

###############################################################################
##
#M  SelfSimilarSemigroup(<A>)
#M  SelfSimilarSemigroup(<A>, <bind_vars>)
##
InstallMethod(SelfSimilarSemigroup, "for [IsMealyAutomaton]", [IsMealyAutomaton],
function(A)
  return SelfSimilarSemigroup(AutomatonList(A), A!.states);
end);

InstallMethod(SelfSimilarSemigroup, "for [IsMealyAutomaton, IsBool]", [IsMealyAutomaton, IsBool],
function(A, bind_vars)
  return SelfSimilarSemigroup(AutomatonList(A), A!.states, bind_vars);
end);

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

###############################################################################
##
#M  IsSemigroupOfSelfSimFamily(<G>)
##
InstallMethod(IsSemigroupOfSelfSimFamily, "for [IsSelfSimSemigroup]",
              [IsSelfSimSemigroup],
function(G)
  return G = SemigroupOfSelfSimFamily(G);
end);

###############################################################################
##
#M  IsSelfSimilar(<G>)
##
InstallMethod(IsSelfSimilar, "for [IsSelfSimSemigroup]",
              [IsSelfSimSemigroup],
function(G)
  local g, i, res;
  res := true;
  for g in GeneratorsOfSemigroup(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 [IsSelfSimSemigroup]",
              [IsSelfSimSemigroup],
function(G)
  return FamilyObj(GeneratorsOfSemigroup(G)[1]);
end);

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

InstallMethod(TopDegreeOfTree, "for [IsSelfSimSemigroup]",
              [IsSelfSimSemigroup],
function(G)
  return DegreeOfTree(UnderlyingSelfSimFamily(G));
end);

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

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

  gens := GeneratorsOfSemigroup(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  ViewObj(<G>)
##
InstallMethod(ViewObj, "for [IsSelfSimSemigroup]",
              [IsSelfSimSemigroup],
function(G)
  local i, gens;
  gens := List(GeneratorsOfSemigroup(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  String(<G>)
##
InstallMethod(String, "for [IsSelfSimSemigroup]", [IsSelfSimSemigroup],
function(G)
  local i, gens, formatone, s;

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

  if IsMonoid(G) then
    gens := GeneratorsOfMonoid(G);
  else
    gens := GeneratorsOfSemigroup(G);
  fi;

  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  PrintObj(<G>)
##
InstallMethod(PrintObj, "for [IsSelfSimilarSemigroup]",
              [IsSelfSimilarSemigroup],
function(G)
  Print("SelfSimilarSemigroup(\"", String(G), "\")");
end);

###############################################################################
##
#M  IsTrivial(G)
##
InstallMethod(IsTrivial, "for [IsSelfSimSemigroup]", [IsSelfSimSemigroup],
function (G)
  local g;
  for g in GeneratorsOfSemigroup(G) do
    if not IsOne(g) then return false; fi;
  od;
  return true;
end);

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

  TryNextMethod();
end);

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

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

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

  fam := UnderlyingSelfSimFamily(G);

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

  if w in SemigroupByGenerators(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);

###############################################################################
##
#M  Random(<G>)
##
InstallMethodWithRandomSource(Random, "for a random source and [IsSelfSimSemigroup]",
              [IsRandomSource, IsSelfSimSemigroup],
function(rs, G)
  local w, monoid, F, gens, pi;
  if IsSelfSimilarSemigroup(G) then
    monoid := UnderlyingFreeMonoid(G);

    if IsTrivial(monoid) then
      w := One(monoid);
    else
      while true do
        w := Random(rs, monoid);
        if not IsOne(w) then
          break;
        fi;
      od;
    fi;
    return SelfSim(w, UnderlyingSelfSimFamily(G));
  else
    gens := GeneratorsOfSemigroup(G);
    F := FreeGroup(Length(gens));
    pi := GroupHomomorphismByImagesNC(F,                      UnderlyingFreeGroup(G),
                                    GeneratorsOfGroup(F),   List(gens, Word)        );
    return SelfSim( Random( rs, SemigroupByGenerators( GeneratorsOfGroup(F)))^pi, UnderlyingSelfSimFamily(G));
  fi;
end);

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

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

###############################################################################
##
#M  UnderlyingFreeGroup( <G> )
##
InstallMethod(UnderlyingFreeGroup, "for [IsSelfSimSemigroup]",
              [IsSelfSimSemigroup],
function(G)
  return UnderlyingSelfSimFamily(G)!.freegroup;
end);

InstallMethod(SphericalIndex, "for [IsSelfSimSemigroup]",
              [IsSelfSimSemigroup],
function(G)
  return SphericalIndex(GeneratorsOfSemigroup(G)[1]);
end);

InstallMethod(DegreeOfTree, "for [IsSelfSimSemigroup]",
              [IsSelfSimSemigroup],
function(G)
  return UnderlyingSelfSimFamily(G)!.deg;
end);

InstallMethod(TopDegreeOfTree, "for [IsSelfSimSemigroup]",
              [IsSelfSimSemigroup],
function(G)
  return UnderlyingSelfSimFamily(G)!.deg;
end);

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

###############################################################################
##
#M  IsFiniteState(<G>)
##
InstallMethod(IsFiniteState, "for [IsSelfSimSemigroup]",
              [IsSelfSimSemigroup],
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;

  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;

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

  for g in gens do
    Add(images, MealyAutomatonLocal(g));
  od;

  H := AutomatonSemigroup(aut_list);

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

  SetIsomorphicAutomSemigroup(G, SemigroupByGenerators(UnderlyingAutomFamily(H)!.automgens{images}));
  SetUnderlyingAutomatonSemigroup(G, H);

# preimages of generators of G in UnderlyingFreeGroup(G)
  gens_in_freegrp := List(GeneratorsOfSemigroup(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;
    if IsOne(x!.word) then
      return states[ Position( UnderlyingAutomFamily(H)!.oldstates, UnderlyingAutomFamily(H)!.trivstate)];
    fi;
    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(GeneratorsOfSemigroup(H), x -> preimage_in_freegrp(x));

  if IsSelfSimilarSemigroup(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 := MappingByFunction(G, SemigroupByGenerators(UnderlyingAutomFamily(H)!.automgens{images}), hom_function, inv_hom_function);

    SetMonomorphismToAutomatonSemigroup(G, hom);
  else
    F := FreeGroup(Length(GeneratorsOfSemigroup(G)));
    #SetCoveringFreeGroup(G, F);

#        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 := MappingByFunction(G, SemigroupByGenerators(UnderlyingAutomFamily(H)!.automgens{images}), hom_function, inv_hom_function);

    SetMonomorphismToAutomatonSemigroup(G, hom);
  fi;

  return true;
end);

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

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

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

#E