Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Древесные оптимизации раздувают программы #332

Open
Mazdaywik opened this issue Nov 21, 2020 · 17 comments
Assignees

Comments

@Mazdaywik
Copy link
Member

Mazdaywik commented Nov 21, 2020

Проблема

Опыт самоприменения с глубокими оптимизациями (-OA + древесные) показал, что компилятор склонен раздувать программы (много примеров в #319, в свёрнутых комментариях). Программы раздуваются по двум причинам:

  • прогонка может мультипликативно увеличить количество предложений,
  • специализация может строить огромное количество переспециализированных экземпляров.

Под переспециализированными экземплярами мы понимаем избыточное количество экземпляров, которые не способствуют каким-либо интересным нетривиальным преобразованиям. Как правило, это специализации по каким-либо аккумуляторным переменным.

Сейчас эта проблема решается ручным анализом причины каждого очередного распухания и купированием его ad hoc (много примеров — коммиты к #319, предлагается даже специальный инструмент для купирования — #331). Но это не дело.

Ожидаемое поведение

Компилятор не должен существенно раздувать программу, раздутие в общем случае должно быть умеренным, не более чем в 10 раз. Но при этом допускать интересные преобразования вроде специализации (в широком смысле) простых интерпретаторов (вроде скрипта древесных оптимизаций).

Понятно, что обеспечить эту численную характеристику (×10) для нетривиальных символьных преобразований нельзя, речь идёт о том, что на наборе типичных программ (сам Рефал-5λ без ручных разметок, MSCP-A, SCP4, другие, которые найдутся) среднее раздутие (размер RASL’а) должно быть не более, чем на порядок, чем при компиляции без древесных оптимизаций.

Но при этом возможность интересных преобразований должна допускаться.

С точки зрения математики обе цели противоречивы: допускать нетривиальные преобразования и ограничивать объём преобразованной программы. Это, вроде как, следует из теоремы Райса-Успенского. Но с точки зрения практики можно найти приемлемый компромисс, когда можно писать и специализируемые интерпретаторы, и программы распухают не сильно.

@Mazdaywik
Copy link
Member Author

Пример распухания из-за специализации

Рассмотрим такую программу:

*$MST_FROM_ENTRY;

$ENTRY Test {
  e.X = <F () e.X>;
}

F {
  (e.Acc) 'a' e.Rest = <G (e.Acc 'A') e.Rest>;
  (e.Acc) 'b' e.Rest = <G (e.Acc 'B') e.Rest>;
  (e.Acc) 'c' e.Rest = <G (e.Acc 'C') e.Rest>;
  (e.Acc) s.1 e.Rest = <F (e.Acc s.1) e.Rest>;
  (e.Acc) /* empty */ = e.Acc;
}

G {
  (e.Acc) 'a' e.Rest = <F (e.Acc A) e.Rest>;
  (e.Acc) 'b' e.Rest = <F (e.Acc B) e.Rest>;
  (e.Acc) 'c' e.Rest = <F (e.Acc C) e.Rest>;
  (e.Acc) s.1 e.Rest = <G (e.Acc s.1) e.Rest>;
  (e.Acc) /* empty */ = e.Acc;
}

Откомпилируем (в папке build), коммит b57a5e9:

..\bin\rlc-core -OAS FG-bloat.ref --log=FG-bloat.log -C

Получится такой результат в логе.

Полный лог

(Лишние пустые строки удалены для читаемости)

Fri Nov 27 22:40:04 2020: AST of file FG-bloat.ref (after tree optimization):
  
  $ENTRY Test {
    e.X#1 = <F@1 e.X#1>;
  }
  
  F {
    (e.Acc#1) 'a' e.Rest#1 = <G@1 (e.Acc#1) e.Rest#1>;
    (e.Acc#1) 'b' e.Rest#1 = <G@2 (e.Acc#1) e.Rest#1>;
    (e.Acc#1) 'c' e.Rest#1 = <G@3 (e.Acc#1) e.Rest#1>;
    (e.Acc#1) s.1#1 e.Rest#1 = <F (e.Acc#1 s.1#1) e.Rest#1>;
    (e.Acc#1) = e.Acc#1;
  }
  
  G {
    (e.Acc#1) 'a' e.Rest#1 = <F@2 (e.Acc#1) e.Rest#1>;
    (e.Acc#1) 'b' e.Rest#1 = <F@3 (e.Acc#1) e.Rest#1>;
    (e.Acc#1) 'c' e.Rest#1 = <F@4 (e.Acc#1) e.Rest#1>;
    (e.Acc#1) s.1#1 e.Rest#1 = <G (e.Acc#1 s.1#1) e.Rest#1>;
    (e.Acc#1) = e.Acc#1;
  }
  
  Test@0 {
  }
  
  F@0 {
  }
  
  G@0 {
  }
  
  F@1 {
    'a' e.Rest#1 = <G@4 e.Rest#1>;
    'b' e.Rest#1 = <G@5 e.Rest#1>;
    'c' e.Rest#1 = <G@6 e.Rest#1>;
    s.1#1 e.Rest#1 = <F (s.1#1) e.Rest#1>;
    /* empty */ = /* empty */;
    e.dyn#1 = <F@0 () e.dyn#1>;
  }
  
  G@1 {
    (e.Acc0#1) 'a' e.Rest#1 = <F (e.Acc0#1 'A' A) e.Rest#1>;
    (e.Acc0#1) 'b' e.Rest#1 = <F (e.Acc0#1 'A' B) e.Rest#1>;
    (e.Acc0#1) 'c' e.Rest#1 = <F (e.Acc0#1 'A' C) e.Rest#1>;
    (e.Acc0#1) s.1#1 e.Rest#1 = <G@7 (e.Acc0#1 'A') s.1#1 e.Rest#1>;
    (e.Acc0#1) = e.Acc0#1 'A';
    (e.Acc0#1) e.dyn#1 = <G@0 (e.Acc0#1 'A') e.dyn#1>;
  }
  
  G@2 {
    (e.Acc0#1) 'a' e.Rest#1 = <F (e.Acc0#1 'B' A) e.Rest#1>;
    (e.Acc0#1) 'b' e.Rest#1 = <F (e.Acc0#1 'B' B) e.Rest#1>;
    (e.Acc0#1) 'c' e.Rest#1 = <F (e.Acc0#1 'B' C) e.Rest#1>;
    (e.Acc0#1) s.1#1 e.Rest#1 = <G@7 (e.Acc0#1 'B') s.1#1 e.Rest#1>;
    (e.Acc0#1) = e.Acc0#1 'B';
    (e.Acc0#1) e.dyn#1 = <G@0 (e.Acc0#1 'B') e.dyn#1>;
  }
  
  G@3 {
    (e.Acc0#1) 'a' e.Rest#1 = <F (e.Acc0#1 'C' A) e.Rest#1>;
    (e.Acc0#1) 'b' e.Rest#1 = <F (e.Acc0#1 'C' B) e.Rest#1>;
    (e.Acc0#1) 'c' e.Rest#1 = <F (e.Acc0#1 'C' C) e.Rest#1>;
    (e.Acc0#1) s.1#1 e.Rest#1 = <G@7 (e.Acc0#1 'C') s.1#1 e.Rest#1>;
    (e.Acc0#1) = e.Acc0#1 'C';
    (e.Acc0#1) e.dyn#1 = <G@0 (e.Acc0#1 'C') e.dyn#1>;
  }
  
  F@2 {
    (e.Acc0#1) 'a' e.Rest#1 = <G (e.Acc0#1 A 'A') e.Rest#1>;
    (e.Acc0#1) 'b' e.Rest#1 = <G (e.Acc0#1 A 'B') e.Rest#1>;
    (e.Acc0#1) 'c' e.Rest#1 = <G (e.Acc0#1 A 'C') e.Rest#1>;
    (e.Acc0#1) s.1#1 e.Rest#1 = <F@5 (e.Acc0#1 A) s.1#1 e.Rest#1>;
    (e.Acc0#1) = e.Acc0#1 A;
    (e.Acc0#1) e.dyn#1 = <F@0 (e.Acc0#1 A) e.dyn#1>;
  }
  
  F@3 {
    (e.Acc0#1) 'a' e.Rest#1 = <G (e.Acc0#1 B 'A') e.Rest#1>;
    (e.Acc0#1) 'b' e.Rest#1 = <G (e.Acc0#1 B 'B') e.Rest#1>;
    (e.Acc0#1) 'c' e.Rest#1 = <G (e.Acc0#1 B 'C') e.Rest#1>;
    (e.Acc0#1) s.1#1 e.Rest#1 = <F@5 (e.Acc0#1 B) s.1#1 e.Rest#1>;
    (e.Acc0#1) = e.Acc0#1 B;
    (e.Acc0#1) e.dyn#1 = <F@0 (e.Acc0#1 B) e.dyn#1>;
  }
  
  F@4 {
    (e.Acc0#1) 'a' e.Rest#1 = <G (e.Acc0#1 C 'A') e.Rest#1>;
    (e.Acc0#1) 'b' e.Rest#1 = <G (e.Acc0#1 C 'B') e.Rest#1>;
    (e.Acc0#1) 'c' e.Rest#1 = <G (e.Acc0#1 C 'C') e.Rest#1>;
    (e.Acc0#1) s.1#1 e.Rest#1 = <F@5 (e.Acc0#1 C) s.1#1 e.Rest#1>;
    (e.Acc0#1) = e.Acc0#1 C;
    (e.Acc0#1) e.dyn#1 = <F@0 (e.Acc0#1 C) e.dyn#1>;
  }
  
  G@4 {
    'a' e.Rest#1 = <F ('A' A) e.Rest#1>;
    'b' e.Rest#1 = <F ('A' B) e.Rest#1>;
    'c' e.Rest#1 = <F ('A' C) e.Rest#1>;
    s.1#1 e.Rest#1 = <G@8 (s.1#1) e.Rest#1>;
    /* empty */ = 'A';
    e.dyn#1 = <G@0 ('A') e.dyn#1>;
  }
  
  G@5 {
    'a' e.Rest#1 = <F ('B' A) e.Rest#1>;
    'b' e.Rest#1 = <F ('B' B) e.Rest#1>;
    'c' e.Rest#1 = <F ('B' C) e.Rest#1>;
    s.1#1 e.Rest#1 = <G@9 (s.1#1) e.Rest#1>;
    /* empty */ = 'B';
    e.dyn#1 = <G@0 ('B') e.dyn#1>;
  }
  
  G@6 {
    'a' e.Rest#1 = <F ('C' A) e.Rest#1>;
    'b' e.Rest#1 = <F ('C' B) e.Rest#1>;
    'c' e.Rest#1 = <F ('C' C) e.Rest#1>;
    s.1#1 e.Rest#1 = <G@10 (s.1#1) e.Rest#1>;
    /* empty */ = 'C';
    e.dyn#1 = <G@0 ('C') e.dyn#1>;
  }
  
  G@7 {
    (e.X#0) s.X#0 'a' e.Rest#1 = <F (e.X#0 s.X#0 A) e.Rest#1>;
    (e.X#0) s.X#0 'b' e.Rest#1 = <F (e.X#0 s.X#0 B) e.Rest#1>;
    (e.X#0) s.X#0 'c' e.Rest#1 = <F (e.X#0 s.X#0 C) e.Rest#1>;
    (e.X#0) s.X#0 s.1#1 e.Rest#1 = <G@7 (e.X#0 s.X#0) s.1#1 e.Rest#1>;
    (e.X#0) s.X#0 = e.X#0 s.X#0;
    (e.X#0) s.X#0 e.dyn#1 = <G@0 (e.X#0 s.X#0) e.dyn#1>;
  }
  
  F@5 {
    (e.X#0) s.X#0 'a' e.Rest#1 = <G (e.X#0 s.X#0 'A') e.Rest#1>;
    (e.X#0) s.X#0 'b' e.Rest#1 = <G (e.X#0 s.X#0 'B') e.Rest#1>;
    (e.X#0) s.X#0 'c' e.Rest#1 = <G (e.X#0 s.X#0 'C') e.Rest#1>;
    (e.X#0) s.X#0 s.1#1 e.Rest#1 = <F@5 (e.X#0 s.X#0) s.1#1 e.Rest#1>;
    (e.X#0) s.X#0 = e.X#0 s.X#0;
    (e.X#0) s.X#0 e.dyn#1 = <F@0 (e.X#0 s.X#0) e.dyn#1>;
  }
  
  G@8 {
    (e.X#0) 'a' e.Rest#1 = <F ('A' e.X#0 A) e.Rest#1>;
    (e.X#0) 'b' e.Rest#1 = <F ('A' e.X#0 B) e.Rest#1>;
    (e.X#0) 'c' e.Rest#1 = <F ('A' e.X#0 C) e.Rest#1>;
    (e.X#0) s.1#1 e.Rest#1 = <G@8 (e.X#0 s.1#1) e.Rest#1>;
    (e.X#0) = 'A' e.X#0;
    (e.X#0) e.dyn#1 = <G@0 ('A' e.X#0) e.dyn#1>;
  }
  
  G@9 {
    (e.X#0) 'a' e.Rest#1 = <F ('B' e.X#0 A) e.Rest#1>;
    (e.X#0) 'b' e.Rest#1 = <F ('B' e.X#0 B) e.Rest#1>;
    (e.X#0) 'c' e.Rest#1 = <F ('B' e.X#0 C) e.Rest#1>;
    (e.X#0) s.1#1 e.Rest#1 = <G@9 (e.X#0 s.1#1) e.Rest#1>;
    (e.X#0) = 'B' e.X#0;
    (e.X#0) e.dyn#1 = <G@0 ('B' e.X#0) e.dyn#1>;
  }
  
  G@10 {
    (e.X#0) 'a' e.Rest#1 = <F ('C' e.X#0 A) e.Rest#1>;
    (e.X#0) 'b' e.Rest#1 = <F ('C' e.X#0 B) e.Rest#1>;
    (e.X#0) 'c' e.Rest#1 = <F ('C' e.X#0 C) e.Rest#1>;
    (e.X#0) s.1#1 e.Rest#1 = <G@10 (e.X#0 s.1#1) e.Rest#1>;
    (e.X#0) = 'C' e.X#0;
    (e.X#0) e.dyn#1 = <G@0 ('C' e.X#0) e.dyn#1>;
  }

Для этой программы построилось 5 (пять) экземпляров функции F и 10 (десять!) экземпляров функции G. И все они в программе используются. Инкрементная разблокировка функций (#228 (comment)) удалила бы G (остался бы заблокирован) и не стала бы создавать F@2, F@3 и F@4. Это немало, но это не много.

Если включить прогонку, то некоторые из функций встроятся… Э-э-э, нет… Почему-то ни одна из функций не была помечена для прогонки, даже F@1. Надо разобраться.


К слову, SCP4 на этом примере тоже даёт некрасивый результат.

Некрасивый результат SCP4
/*
$ENTRY Go {
 = <Prout <Test e.1 >> ;
}
*/

***
/*
*  InputFormat: <Test e.41 >
*  OutputFormat: ==> e.0 
*/


$ENTRY Test {
 'aa' e.41 =  <F45 (e.41)>;
 'ab' e.41 =  <F166 (e.41)>;
 'ac' e.41 =  <F287 (e.41)>;
 'a' s.102 e.41 =  <F408 (e.41) s.102>;
 'a' =  'A';
 'ba' e.41 =  <F536 (e.41)>;
 'bb' e.41 =  <F657 (e.41)>;
 'bc' e.41 =  <F778 (e.41)>;
 'b' s.204 e.41 =  <F899 (e.41) s.204>;
 'b' =  'B';
 'ca' e.41 =  <F1027 (e.41)>;
 'cb' e.41 =  <F1148 (e.41)>;
 'cc' e.41 =  <F1269 (e.41)>;
 'c' s.306 e.41 =  <F1390 (e.41) s.306>;
 'c' =  'C';
 s.101 'a' e.41 =  <F1518 (e.41) s.101>;
 s.101 'b' e.41 =  <F1639 (e.41) s.101>;
 s.101 'c' e.41 =  <F1760 (e.41) s.101>;
 s.101 s.408 e.41 =  <F1881 (e.41) s.101 s.408>;
 s.101 =  s.101;
 = ;
}

/*
*  InputFormat: <F1924 (e.516 ) s.517 s.518 s.519 e.520 >
*  OutputFormat: ==> e.0 
*/
F1924 {
 ('a' e.516) s.517 s.518 s.519 e.520 =  <F1881 (e.516) s.517 s.518 e.520 s.519 A>;
 ('b' e.516) s.517 s.518 s.519 e.520 =  <F1881 (e.516) s.517 s.518 e.520 s.519 B>;
 ('c' e.516) s.517 s.518 s.519 e.520 =  <F1881 (e.516) s.517 s.518 e.520 s.519 C>;
 (s.521 e.516) s.517 s.518 s.519 e.520 =  <F1924 (e.516) s.517 s.518 s.521 e.520 s.519>;
 () s.517 s.518 s.519 e.520 =  s.517 s.518 e.520 s.519;
}

/*
*  InputFormat: <F1881 (e.504 ) s.505 s.506 e.507 >
*  OutputFormat: ==> e.0 
*/
F1881 {
 ('a' e.504) s.505 s.506 e.507 =  <F1924 (e.504) s.505 s.506 'A' e.507>;
 ('b' e.504) s.505 s.506 e.507 =  <F1924 (e.504) s.505 s.506 'B' e.507>;
 ('c' e.504) s.505 s.506 e.507 =  <F1924 (e.504) s.505 s.506 'C' e.507>;
 (s.508 e.504) s.505 s.506 e.507 =  <F1881 (e.504) s.505 s.506 e.507 s.508>;
 () s.505 s.506 e.507 =  s.505 s.506 e.507;
}

/*
*  InputFormat: <F1803 (e.485 ) s.486 s.487 e.488 >
*  OutputFormat: ==> e.0 
*/
F1803 {
 ('a' e.485) s.486 s.487 e.488 =  <F1760 (e.485) s.486 e.488 s.487 'A'>;
 ('b' e.485) s.486 s.487 e.488 =  <F1760 (e.485) s.486 e.488 s.487 'B'>;
 ('c' e.485) s.486 s.487 e.488 =  <F1760 (e.485) s.486 e.488 s.487 'C'>;
 (s.489 e.485) s.486 s.487 e.488 =  <F1803 (e.485) s.486 s.489 e.488 s.487>;
 () s.486 s.487 e.488 =  s.486 'C' e.488 s.487;
}

/*
*  InputFormat: <F1760 (e.474 ) s.475 e.476 >
*  OutputFormat: ==> e.0 
*/
F1760 {
 ('a' e.474) s.475 e.476 =  <F1803 (e.474) s.475 A e.476>;
 ('b' e.474) s.475 e.476 =  <F1803 (e.474) s.475 B e.476>;
 ('c' e.474) s.475 e.476 =  <F1803 (e.474) s.475 C e.476>;
 (s.477 e.474) s.475 e.476 =  <F1760 (e.474) s.475 e.476 s.477>;
 () s.475 e.476 =  s.475 'C' e.476;
}

/*
*  InputFormat: <F1682 (e.455 ) s.456 s.457 e.458 >
*  OutputFormat: ==> e.0 
*/
F1682 {
 ('a' e.455) s.456 s.457 e.458 =  <F1639 (e.455) s.456 e.458 s.457 'A'>;
 ('b' e.455) s.456 s.457 e.458 =  <F1639 (e.455) s.456 e.458 s.457 'B'>;
 ('c' e.455) s.456 s.457 e.458 =  <F1639 (e.455) s.456 e.458 s.457 'C'>;
 (s.459 e.455) s.456 s.457 e.458 =  <F1682 (e.455) s.456 s.459 e.458 s.457>;
 () s.456 s.457 e.458 =  s.456 'B' e.458 s.457;
}

/*
*  InputFormat: <F1639 (e.444 ) s.445 e.446 >
*  OutputFormat: ==> e.0 
*/
F1639 {
 ('a' e.444) s.445 e.446 =  <F1682 (e.444) s.445 A e.446>;
 ('b' e.444) s.445 e.446 =  <F1682 (e.444) s.445 B e.446>;
 ('c' e.444) s.445 e.446 =  <F1682 (e.444) s.445 C e.446>;
 (s.447 e.444) s.445 e.446 =  <F1639 (e.444) s.445 e.446 s.447>;
 () s.445 e.446 =  s.445 'B' e.446;
}

/*
*  InputFormat: <F1561 (e.425 ) s.426 s.427 e.428 >
*  OutputFormat: ==> e.0 
*/
F1561 {
 ('a' e.425) s.426 s.427 e.428 =  <F1518 (e.425) s.426 e.428 s.427 'A'>;
 ('b' e.425) s.426 s.427 e.428 =  <F1518 (e.425) s.426 e.428 s.427 'B'>;
 ('c' e.425) s.426 s.427 e.428 =  <F1518 (e.425) s.426 e.428 s.427 'C'>;
 (s.429 e.425) s.426 s.427 e.428 =  <F1561 (e.425) s.426 s.429 e.428 s.427>;
 () s.426 s.427 e.428 =  s.426 'A' e.428 s.427;
}

/*
*  InputFormat: <F1518 (e.414 ) s.415 e.416 >
*  OutputFormat: ==> e.0 
*/
F1518 {
 ('a' e.414) s.415 e.416 =  <F1561 (e.414) s.415 A e.416>;
 ('b' e.414) s.415 e.416 =  <F1561 (e.414) s.415 B e.416>;
 ('c' e.414) s.415 e.416 =  <F1561 (e.414) s.415 C e.416>;
 (s.417 e.414) s.415 e.416 =  <F1518 (e.414) s.415 e.416 s.417>;
 () s.415 e.416 =  s.415 'A' e.416;
}

/*
*  InputFormat: <F1433 (e.396 ) s.397 s.398 e.399 >
*  OutputFormat: ==> e.0 
*/
F1433 {
 ('a' e.396) s.397 s.398 e.399 =  <F1390 (e.396) s.397 e.399 s.398 'A'>;
 ('b' e.396) s.397 s.398 e.399 =  <F1390 (e.396) s.397 e.399 s.398 'B'>;
 ('c' e.396) s.397 s.398 e.399 =  <F1390 (e.396) s.397 e.399 s.398 'C'>;
 (s.400 e.396) s.397 s.398 e.399 =  <F1433 (e.396) s.397 s.400 e.399 s.398>;
 () s.397 s.398 e.399 =  'C' s.397 e.399 s.398;
}

/*
*  InputFormat: <F1390 (e.386 ) s.387 e.388 >
*  OutputFormat: ==> e.0 
*/
F1390 {
 ('a' e.386) s.387 e.388 =  <F1433 (e.386) s.387 A e.388>;
 ('b' e.386) s.387 e.388 =  <F1433 (e.386) s.387 B e.388>;
 ('c' e.386) s.387 e.388 =  <F1433 (e.386) s.387 C e.388>;
 (s.389 e.386) s.387 e.388 =  <F1390 (e.386) s.387 e.388 s.389>;
 () s.387 e.388 =  'C' s.387 e.388;
}

/*
*  InputFormat: <F1312 (e.370 ) s.371 e.372 >
*  OutputFormat: ==> e.0 
*/
F1312 {
 ('a' e.370) s.371 e.372 =  <F1269 (e.370) e.372 s.371 A>;
 ('b' e.370) s.371 e.372 =  <F1269 (e.370) e.372 s.371 B>;
 ('c' e.370) s.371 e.372 =  <F1269 (e.370) e.372 s.371 C>;
 (s.373 e.370) s.371 e.372 =  <F1312 (e.370) s.373 e.372 s.371>;
 () s.371 e.372 =  'C' C e.372 s.371;
}

/*
*  InputFormat: <F1269 (e.361 ) e.362 >
*  OutputFormat: ==> e.0 
*/
F1269 {
 ('a' e.361) e.362 =  <F1312 (e.361) 'A' e.362>;
 ('b' e.361) e.362 =  <F1312 (e.361) 'B' e.362>;
 ('c' e.361) e.362 =  <F1312 (e.361) 'C' e.362>;
 (s.363 e.361) e.362 =  <F1269 (e.361) e.362 s.363>;
 () e.362 =  'C' C e.362;
}

/*
*  InputFormat: <F1191 (e.345 ) s.346 e.347 >
*  OutputFormat: ==> e.0 
*/
F1191 {
 ('a' e.345) s.346 e.347 =  <F1148 (e.345) e.347 s.346 A>;
 ('b' e.345) s.346 e.347 =  <F1148 (e.345) e.347 s.346 B>;
 ('c' e.345) s.346 e.347 =  <F1148 (e.345) e.347 s.346 C>;
 (s.348 e.345) s.346 e.347 =  <F1191 (e.345) s.348 e.347 s.346>;
 () s.346 e.347 =  'C' B e.347 s.346;
}

/*
*  InputFormat: <F1148 (e.336 ) e.337 >
*  OutputFormat: ==> e.0 
*/
F1148 {
 ('a' e.336) e.337 =  <F1191 (e.336) 'A' e.337>;
 ('b' e.336) e.337 =  <F1191 (e.336) 'B' e.337>;
 ('c' e.336) e.337 =  <F1191 (e.336) 'C' e.337>;
 (s.338 e.336) e.337 =  <F1148 (e.336) e.337 s.338>;
 () e.337 =  'C' B e.337;
}

/*
*  InputFormat: <F1070 (e.320 ) s.321 e.322 >
*  OutputFormat: ==> e.0 
*/
F1070 {
 ('a' e.320) s.321 e.322 =  <F1027 (e.320) e.322 s.321 A>;
 ('b' e.320) s.321 e.322 =  <F1027 (e.320) e.322 s.321 B>;
 ('c' e.320) s.321 e.322 =  <F1027 (e.320) e.322 s.321 C>;
 (s.323 e.320) s.321 e.322 =  <F1070 (e.320) s.323 e.322 s.321>;
 () s.321 e.322 =  'C' A e.322 s.321;
}

/*
*  InputFormat: <F1027 (e.311 ) e.312 >
*  OutputFormat: ==> e.0 
*/
F1027 {
 ('a' e.311) e.312 =  <F1070 (e.311) 'A' e.312>;
 ('b' e.311) e.312 =  <F1070 (e.311) 'B' e.312>;
 ('c' e.311) e.312 =  <F1070 (e.311) 'C' e.312>;
 (s.313 e.311) e.312 =  <F1027 (e.311) e.312 s.313>;
 () e.312 =  'C' A e.312;
}

/*
*  InputFormat: <F942 (e.294 ) s.295 s.296 e.297 >
*  OutputFormat: ==> e.0 
*/
F942 {
 ('a' e.294) s.295 s.296 e.297 =  <F899 (e.294) s.295 e.297 s.296 'A'>;
 ('b' e.294) s.295 s.296 e.297 =  <F899 (e.294) s.295 e.297 s.296 'B'>;
 ('c' e.294) s.295 s.296 e.297 =  <F899 (e.294) s.295 e.297 s.296 'C'>;
 (s.298 e.294) s.295 s.296 e.297 =  <F942 (e.294) s.295 s.298 e.297 s.296>;
 () s.295 s.296 e.297 =  'B' s.295 e.297 s.296;
}

/*
*  InputFormat: <F899 (e.284 ) s.285 e.286 >
*  OutputFormat: ==> e.0 
*/
F899 {
 ('a' e.284) s.285 e.286 =  <F942 (e.284) s.285 A e.286>;
 ('b' e.284) s.285 e.286 =  <F942 (e.284) s.285 B e.286>;
 ('c' e.284) s.285 e.286 =  <F942 (e.284) s.285 C e.286>;
 (s.287 e.284) s.285 e.286 =  <F899 (e.284) s.285 e.286 s.287>;
 () s.285 e.286 =  'B' s.285 e.286;
}

/*
*  InputFormat: <F821 (e.268 ) s.269 e.270 >
*  OutputFormat: ==> e.0 
*/
F821 {
 ('a' e.268) s.269 e.270 =  <F778 (e.268) e.270 s.269 A>;
 ('b' e.268) s.269 e.270 =  <F778 (e.268) e.270 s.269 B>;
 ('c' e.268) s.269 e.270 =  <F778 (e.268) e.270 s.269 C>;
 (s.271 e.268) s.269 e.270 =  <F821 (e.268) s.271 e.270 s.269>;
 () s.269 e.270 =  'B' C e.270 s.269;
}

/*
*  InputFormat: <F778 (e.259 ) e.260 >
*  OutputFormat: ==> e.0 
*/
F778 {
 ('a' e.259) e.260 =  <F821 (e.259) 'A' e.260>;
 ('b' e.259) e.260 =  <F821 (e.259) 'B' e.260>;
 ('c' e.259) e.260 =  <F821 (e.259) 'C' e.260>;
 (s.261 e.259) e.260 =  <F778 (e.259) e.260 s.261>;
 () e.260 =  'B' C e.260;
}

/*
*  InputFormat: <F700 (e.243 ) s.244 e.245 >
*  OutputFormat: ==> e.0 
*/
F700 {
 ('a' e.243) s.244 e.245 =  <F657 (e.243) e.245 s.244 A>;
 ('b' e.243) s.244 e.245 =  <F657 (e.243) e.245 s.244 B>;
 ('c' e.243) s.244 e.245 =  <F657 (e.243) e.245 s.244 C>;
 (s.246 e.243) s.244 e.245 =  <F700 (e.243) s.246 e.245 s.244>;
 () s.244 e.245 =  'B' B e.245 s.244;
}

/*
*  InputFormat: <F657 (e.234 ) e.235 >
*  OutputFormat: ==> e.0 
*/
F657 {
 ('a' e.234) e.235 =  <F700 (e.234) 'A' e.235>;
 ('b' e.234) e.235 =  <F700 (e.234) 'B' e.235>;
 ('c' e.234) e.235 =  <F700 (e.234) 'C' e.235>;
 (s.236 e.234) e.235 =  <F657 (e.234) e.235 s.236>;
 () e.235 =  'B' B e.235;
}

/*
*  InputFormat: <F579 (e.218 ) s.219 e.220 >
*  OutputFormat: ==> e.0 
*/
F579 {
 ('a' e.218) s.219 e.220 =  <F536 (e.218) e.220 s.219 A>;
 ('b' e.218) s.219 e.220 =  <F536 (e.218) e.220 s.219 B>;
 ('c' e.218) s.219 e.220 =  <F536 (e.218) e.220 s.219 C>;
 (s.221 e.218) s.219 e.220 =  <F579 (e.218) s.221 e.220 s.219>;
 () s.219 e.220 =  'B' A e.220 s.219;
}

/*
*  InputFormat: <F536 (e.209 ) e.210 >
*  OutputFormat: ==> e.0 
*/
F536 {
 ('a' e.209) e.210 =  <F579 (e.209) 'A' e.210>;
 ('b' e.209) e.210 =  <F579 (e.209) 'B' e.210>;
 ('c' e.209) e.210 =  <F579 (e.209) 'C' e.210>;
 (s.211 e.209) e.210 =  <F536 (e.209) e.210 s.211>;
 () e.210 =  'B' A e.210;
}

/*
*  InputFormat: <F451 (e.192 ) s.193 s.194 e.195 >
*  OutputFormat: ==> e.0 
*/
F451 {
 ('a' e.192) s.193 s.194 e.195 =  <F408 (e.192) s.193 e.195 s.194 'A'>;
 ('b' e.192) s.193 s.194 e.195 =  <F408 (e.192) s.193 e.195 s.194 'B'>;
 ('c' e.192) s.193 s.194 e.195 =  <F408 (e.192) s.193 e.195 s.194 'C'>;
 (s.196 e.192) s.193 s.194 e.195 =  <F451 (e.192) s.193 s.196 e.195 s.194>;
 () s.193 s.194 e.195 =  'A' s.193 e.195 s.194;
}

/*
*  InputFormat: <F408 (e.182 ) s.183 e.184 >
*  OutputFormat: ==> e.0 
*/
F408 {
 ('a' e.182) s.183 e.184 =  <F451 (e.182) s.183 A e.184>;
 ('b' e.182) s.183 e.184 =  <F451 (e.182) s.183 B e.184>;
 ('c' e.182) s.183 e.184 =  <F451 (e.182) s.183 C e.184>;
 (s.185 e.182) s.183 e.184 =  <F408 (e.182) s.183 e.184 s.185>;
 () s.183 e.184 =  'A' s.183 e.184;
}

/*
*  InputFormat: <F330 (e.166 ) s.167 e.168 >
*  OutputFormat: ==> e.0 
*/
F330 {
 ('a' e.166) s.167 e.168 =  <F287 (e.166) e.168 s.167 A>;
 ('b' e.166) s.167 e.168 =  <F287 (e.166) e.168 s.167 B>;
 ('c' e.166) s.167 e.168 =  <F287 (e.166) e.168 s.167 C>;
 (s.169 e.166) s.167 e.168 =  <F330 (e.166) s.169 e.168 s.167>;
 () s.167 e.168 =  'A' C e.168 s.167;
}

/*
*  InputFormat: <F287 (e.157 ) e.158 >
*  OutputFormat: ==> e.0 
*/
F287 {
 ('a' e.157) e.158 =  <F330 (e.157) 'A' e.158>;
 ('b' e.157) e.158 =  <F330 (e.157) 'B' e.158>;
 ('c' e.157) e.158 =  <F330 (e.157) 'C' e.158>;
 (s.159 e.157) e.158 =  <F287 (e.157) e.158 s.159>;
 () e.158 =  'A' C e.158;
}

/*
*  InputFormat: <F209 (e.141 ) s.142 e.143 >
*  OutputFormat: ==> e.0 
*/
F209 {
 ('a' e.141) s.142 e.143 =  <F166 (e.141) e.143 s.142 A>;
 ('b' e.141) s.142 e.143 =  <F166 (e.141) e.143 s.142 B>;
 ('c' e.141) s.142 e.143 =  <F166 (e.141) e.143 s.142 C>;
 (s.144 e.141) s.142 e.143 =  <F209 (e.141) s.144 e.143 s.142>;
 () s.142 e.143 =  'A' B e.143 s.142;
}

/*
*  InputFormat: <F166 (e.132 ) e.133 >
*  OutputFormat: ==> e.0 
*/
F166 {
 ('a' e.132) e.133 =  <F209 (e.132) 'A' e.133>;
 ('b' e.132) e.133 =  <F209 (e.132) 'B' e.133>;
 ('c' e.132) e.133 =  <F209 (e.132) 'C' e.133>;
 (s.134 e.132) e.133 =  <F166 (e.132) e.133 s.134>;
 () e.133 =  'A' B e.133;
}

/*
*  InputFormat: <F88 (e.116 ) s.117 e.118 >
*  OutputFormat: ==> e.0 
*/
F88 {
 ('a' e.116) s.117 e.118 =  <F45 (e.116) e.118 s.117 A>;
 ('b' e.116) s.117 e.118 =  <F45 (e.116) e.118 s.117 B>;
 ('c' e.116) s.117 e.118 =  <F45 (e.116) e.118 s.117 C>;
 (s.119 e.116) s.117 e.118 =  <F88 (e.116) s.119 e.118 s.117>;
 () s.117 e.118 =  'A' A e.118 s.117;
}

/*
*  InputFormat: <F45 (e.107 ) e.108 >
*  OutputFormat: ==> e.0 
*/
F45 {
 ('a' e.107) e.108 =  <F88 (e.107) 'A' e.108>;
 ('b' e.107) e.108 =  <F88 (e.107) 'B' e.108>;
 ('c' e.107) e.108 =  <F88 (e.107) 'C' e.108>;
 (s.109 e.107) e.108 =  <F45 (e.107) e.108 s.109>;
 () e.108 =  'A' A e.108;
}

****************************** The End ************************************

А вот MSCP-A даёт красивый результат:

Красивый результат MSCP-A
$ENTRY Go {
 e.x1 =  <F_12345_0 () (e.x1)>;
}


G_12345_0 {
 (e.x1) (e.x2) ('a' e.x3) =  <F_12345_0 (e.x1 'A' e.x2 A) (e.x3)>;
 (e.x1) (e.x2) ('b' e.x3) =  <F_12345_0 (e.x1 'A' e.x2 B) (e.x3)>;
 (e.x1) (e.x2) ('c' e.x3) =  <F_12345_0 (e.x1 'A' e.x2 C) (e.x3)>;
 (e.x1) (e.x2) (s.z1 e.x3) =  <G_12345_0 (e.x1) (e.x2 s.z1) (e.x3)>;
 (e.x1) (e.x2) () =  e.x1 'A' e.x2;
}


G_12345_1 {
 (e.x1) (e.x2) ('a' e.x3) =  <F_12345_0 (e.x1 'B' e.x2 A) (e.x3)>;
 (e.x1) (e.x2) ('b' e.x3) =  <F_12345_0 (e.x1 'B' e.x2 B) (e.x3)>;
 (e.x1) (e.x2) ('c' e.x3) =  <F_12345_0 (e.x1 'B' e.x2 C) (e.x3)>;
 (e.x1) (e.x2) (s.z1 e.x3) =  <G_12345_1 (e.x1) (e.x2 s.z1) (e.x3)>;
 (e.x1) (e.x2) () =  e.x1 'B' e.x2;
}


F_12345_0 {
 (e.x1) ('a' e.x2) =  <G_12345_0 (e.x1) () (e.x2)>;
 (e.x1) ('b' e.x2) =  <G_12345_1 (e.x1) () (e.x2)>;
 (e.x1) ('c' e.x2) =  <G_12345_2 (e.x1) () (e.x2)>;
 (e.x1) (s.z1 e.x2) =  <F_12345_0 (e.x1 s.z1) (e.x2)>;
 (e.x1) () =  e.x1;
}


G_12345_2 {
 (e.x1) (e.x2) ('a' e.x3) =  <F_12345_0 (e.x1 'C' e.x2 A) (e.x3)>;
 (e.x1) (e.x2) ('b' e.x3) =  <F_12345_0 (e.x1 'C' e.x2 B) (e.x3)>;
 (e.x1) (e.x2) ('c' e.x3) =  <F_12345_0 (e.x1 'C' e.x2 C) (e.x3)>;
 (e.x1) (e.x2) (s.z1 e.x3) =  <G_12345_2 (e.x1) (e.x2 s.z1) (e.x3)>;
 (e.x1) (e.x2) () =  e.x1 'C' e.x2;
}

/* This file was generated by MSCP at Fri Nov 27 22:20:53 2020.*/

/* Elapsed time of embeddings is 16.0.*/

/* Elapsed time of generalizations is 561.0.*/

Есть более простой пример, на котором SCP4 тоже выдаёт некрасивый результат, но Рефал-5λ и MSCP-A справляются.

Пример
*$MST_FROM_ENTRY;

$ENTRY Test {
  e.X = <F () e.X>;
}

F {
  (e.Acc) 'a' e.Rest = <F (e.Acc 'A') e.Rest>;
  (e.Acc) 'b' e.Rest = <F (e.Acc 'B') e.Rest>;
  (e.Acc) 'c' e.Rest = <F (e.Acc 'C') e.Rest>;
  (e.Acc) s.1 e.Rest = <F (e.Acc s.1) e.Rest>;
  (e.Acc) /* empty */ = e.Acc;
}
Некрасивый результат SCP4
/*
$ENTRY Go {
 = <Prout <Test e.1 >> ;
}
*/

***
/*
*  InputFormat: <Test e.41 >
*  OutputFormat: ==> e.0 
*/


$ENTRY Test {
 'aa' e.41 =  'AA' <F38 (e.41)>;
 'ab' e.41 =  'AB' <F88 (e.41)>;
 'ac' e.41 =  'AC' <F138 (e.41)>;
 'a' s.102 e.41, <F188 (e.41) s.102> : s.165 (e.166) =  'A' s.165 e.166;
 'a' =  'A';
 'ba' e.41 =  'BA' <F245 (e.41)>;
 'bb' e.41 =  'BB' <F295 (e.41)>;
 'bc' e.41 =  'BC' <F345 (e.41)>;
 'b' s.167 e.41, <F395 (e.41) s.167> : s.230 (e.231) =  'B' s.230 e.231;
 'b' =  'B';
 'ca' e.41 =  'CA' <F452 (e.41)>;
 'cb' e.41 =  'CB' <F502 (e.41)>;
 'cc' e.41 =  'CC' <F552 (e.41)>;
 'c' s.232 e.41, <F602 (e.41) s.232> : s.295 (e.296) =  'C' s.295 e.296;
 'c' =  'C';
 s.101 'a' e.41, <F659 (e.41) s.101> : s.317 (e.318) =  s.317 'A' e.318;
 s.101 'b' e.41, <F709 (e.41) s.101> : s.338 (e.339) =  s.338 'B' e.339;
 s.101 'c' e.41, <F759 (e.41) s.101> : s.359 (e.360) =  s.359 'C' e.360;
 s.101 s.297 e.41, <F809 (e.41) s.101 s.297> : s.383 s.384 (e.385) =  s.383 s.384 e.385;
 s.101 =  s.101;
 = ;
}

/*
*  InputFormat: <F809 (e.365 ) s.366 s.367 e.368 >
*  OutputFormat: ==> s.383 s.384 (e.385 ) 
*/
F809 {
 ('a' e.365) s.366 s.367 e.368, <F809 (e.365) s.366 s.367 e.368 'A'> : s.510 s.511 (e.512) =  s.510 s.511 (e.512);
 ('b' e.365) s.366 s.367 e.368, <F809 (e.365) s.366 s.367 e.368 'B'> : s.513 s.514 (e.515) =  s.513 s.514 (e.515);
 ('c' e.365) s.366 s.367 e.368, <F809 (e.365) s.366 s.367 e.368 'C'> : s.516 s.517 (e.518) =  s.516 s.517 (e.518);
 (s.369 e.365) s.366 s.367 e.368, <F809 (e.365) s.366 s.367 e.368 s.369> : s.519 s.520 (e.521) =  s.519 s.520 (e.521);
 () s.366 s.367 e.368 =  s.366 s.367 (e.368);
}

/*
*  InputFormat: <F759 (e.344 ) s.345 e.346 >
*  OutputFormat: ==> s.359 (e.360 ) 
*/
F759 {
 ('a' e.344) s.345 e.346, <F759 (e.344) s.345 e.346 'A'> : s.502 (e.503) =  s.502 (e.503);
 ('b' e.344) s.345 e.346, <F759 (e.344) s.345 e.346 'B'> : s.504 (e.505) =  s.504 (e.505);
 ('c' e.344) s.345 e.346, <F759 (e.344) s.345 e.346 'C'> : s.506 (e.507) =  s.506 (e.507);
 (s.347 e.344) s.345 e.346, <F759 (e.344) s.345 e.346 s.347> : s.508 (e.509) =  s.508 (e.509);
 () s.345 e.346 =  s.345 (e.346);
}

/*
*  InputFormat: <F709 (e.323 ) s.324 e.325 >
*  OutputFormat: ==> s.338 (e.339 ) 
*/
F709 {
 ('a' e.323) s.324 e.325, <F709 (e.323) s.324 e.325 'A'> : s.494 (e.495) =  s.494 (e.495);
 ('b' e.323) s.324 e.325, <F709 (e.323) s.324 e.325 'B'> : s.496 (e.497) =  s.496 (e.497);
 ('c' e.323) s.324 e.325, <F709 (e.323) s.324 e.325 'C'> : s.498 (e.499) =  s.498 (e.499);
 (s.326 e.323) s.324 e.325, <F709 (e.323) s.324 e.325 s.326> : s.500 (e.501) =  s.500 (e.501);
 () s.324 e.325 =  s.324 (e.325);
}

/*
*  InputFormat: <F659 (e.302 ) s.303 e.304 >
*  OutputFormat: ==> s.317 (e.318 ) 
*/
F659 {
 ('a' e.302) s.303 e.304, <F659 (e.302) s.303 e.304 'A'> : s.486 (e.487) =  s.486 (e.487);
 ('b' e.302) s.303 e.304, <F659 (e.302) s.303 e.304 'B'> : s.488 (e.489) =  s.488 (e.489);
 ('c' e.302) s.303 e.304, <F659 (e.302) s.303 e.304 'C'> : s.490 (e.491) =  s.490 (e.491);
 (s.305 e.302) s.303 e.304, <F659 (e.302) s.303 e.304 s.305> : s.492 (e.493) =  s.492 (e.493);
 () s.303 e.304 =  s.303 (e.304);
}

/*
*  InputFormat: <F602 (e.281 ) s.282 e.283 >
*  OutputFormat: ==> s.295 (e.296 ) 
*/
F602 {
 ('a' e.281) s.282 e.283, <F602 (e.281) s.282 e.283 'A'> : s.478 (e.479) =  s.478 (e.479);
 ('b' e.281) s.282 e.283, <F602 (e.281) s.282 e.283 'B'> : s.480 (e.481) =  s.480 (e.481);
 ('c' e.281) s.282 e.283, <F602 (e.281) s.282 e.283 'C'> : s.482 (e.483) =  s.482 (e.483);
 (s.284 e.281) s.282 e.283, <F602 (e.281) s.282 e.283 s.284> : s.484 (e.485) =  s.484 (e.485);
 () s.282 e.283 =  s.282 (e.283);
}

/*
*  InputFormat: <F552 (e.266 ) e.267 >
*  OutputFormat: ==> e.277 
*/
F552 {
 ('a' e.266) e.267 =  <F552 (e.266) e.267 'A'>;
 ('b' e.266) e.267 =  <F552 (e.266) e.267 'B'>;
 ('c' e.266) e.267 =  <F552 (e.266) e.267 'C'>;
 (s.268 e.266) e.267 =  <F552 (e.266) e.267 s.268>;
 () e.267 =  e.267;
}

/*
*  InputFormat: <F502 (e.251 ) e.252 >
*  OutputFormat: ==> e.262 
*/
F502 {
 ('a' e.251) e.252 =  <F502 (e.251) e.252 'A'>;
 ('b' e.251) e.252 =  <F502 (e.251) e.252 'B'>;
 ('c' e.251) e.252 =  <F502 (e.251) e.252 'C'>;
 (s.253 e.251) e.252 =  <F502 (e.251) e.252 s.253>;
 () e.252 =  e.252;
}

/*
*  InputFormat: <F452 (e.236 ) e.237 >
*  OutputFormat: ==> e.247 
*/
F452 {
 ('a' e.236) e.237 =  <F452 (e.236) e.237 'A'>;
 ('b' e.236) e.237 =  <F452 (e.236) e.237 'B'>;
 ('c' e.236) e.237 =  <F452 (e.236) e.237 'C'>;
 (s.238 e.236) e.237 =  <F452 (e.236) e.237 s.238>;
 () e.237 =  e.237;
}

/*
*  InputFormat: <F395 (e.216 ) s.217 e.218 >
*  OutputFormat: ==> s.230 (e.231 ) 
*/
F395 {
 ('a' e.216) s.217 e.218, <F395 (e.216) s.217 e.218 'A'> : s.458 (e.459) =  s.458 (e.459);
 ('b' e.216) s.217 e.218, <F395 (e.216) s.217 e.218 'B'> : s.460 (e.461) =  s.460 (e.461);
 ('c' e.216) s.217 e.218, <F395 (e.216) s.217 e.218 'C'> : s.462 (e.463) =  s.462 (e.463);
 (s.219 e.216) s.217 e.218, <F395 (e.216) s.217 e.218 s.219> : s.464 (e.465) =  s.464 (e.465);
 () s.217 e.218 =  s.217 (e.218);
}

/*
*  InputFormat: <F345 (e.201 ) e.202 >
*  OutputFormat: ==> e.212 
*/
F345 {
 ('a' e.201) e.202 =  <F345 (e.201) e.202 'A'>;
 ('b' e.201) e.202 =  <F345 (e.201) e.202 'B'>;
 ('c' e.201) e.202 =  <F345 (e.201) e.202 'C'>;
 (s.203 e.201) e.202 =  <F345 (e.201) e.202 s.203>;
 () e.202 =  e.202;
}

/*
*  InputFormat: <F295 (e.186 ) e.187 >
*  OutputFormat: ==> e.197 
*/
F295 {
 ('a' e.186) e.187 =  <F295 (e.186) e.187 'A'>;
 ('b' e.186) e.187 =  <F295 (e.186) e.187 'B'>;
 ('c' e.186) e.187 =  <F295 (e.186) e.187 'C'>;
 (s.188 e.186) e.187 =  <F295 (e.186) e.187 s.188>;
 () e.187 =  e.187;
}

/*
*  InputFormat: <F245 (e.171 ) e.172 >
*  OutputFormat: ==> e.182 
*/
F245 {
 ('a' e.171) e.172 =  <F245 (e.171) e.172 'A'>;
 ('b' e.171) e.172 =  <F245 (e.171) e.172 'B'>;
 ('c' e.171) e.172 =  <F245 (e.171) e.172 'C'>;
 (s.173 e.171) e.172 =  <F245 (e.171) e.172 s.173>;
 () e.172 =  e.172;
}

/*
*  InputFormat: <F188 (e.151 ) s.152 e.153 >
*  OutputFormat: ==> s.165 (e.166 ) 
*/
F188 {
 ('a' e.151) s.152 e.153, <F188 (e.151) s.152 e.153 'A'> : s.438 (e.439) =  s.438 (e.439);
 ('b' e.151) s.152 e.153, <F188 (e.151) s.152 e.153 'B'> : s.440 (e.441) =  s.440 (e.441);
 ('c' e.151) s.152 e.153, <F188 (e.151) s.152 e.153 'C'> : s.442 (e.443) =  s.442 (e.443);
 (s.154 e.151) s.152 e.153, <F188 (e.151) s.152 e.153 s.154> : s.444 (e.445) =  s.444 (e.445);
 () s.152 e.153 =  s.152 (e.153);
}

/*
*  InputFormat: <F138 (e.136 ) e.137 >
*  OutputFormat: ==> e.147 
*/
F138 {
 ('a' e.136) e.137 =  <F138 (e.136) e.137 'A'>;
 ('b' e.136) e.137 =  <F138 (e.136) e.137 'B'>;
 ('c' e.136) e.137 =  <F138 (e.136) e.137 'C'>;
 (s.138 e.136) e.137 =  <F138 (e.136) e.137 s.138>;
 () e.137 =  e.137;
}

/*
*  InputFormat: <F88 (e.121 ) e.122 >
*  OutputFormat: ==> e.132 
*/
F88 {
 ('a' e.121) e.122 =  <F88 (e.121) e.122 'A'>;
 ('b' e.121) e.122 =  <F88 (e.121) e.122 'B'>;
 ('c' e.121) e.122 =  <F88 (e.121) e.122 'C'>;
 (s.123 e.121) e.122 =  <F88 (e.121) e.122 s.123>;
 () e.122 =  e.122;
}

/*
*  InputFormat: <F38 (e.106 ) e.107 >
*  OutputFormat: ==> e.117 
*/
F38 {
 ('a' e.106) e.107 =  <F38 (e.106) e.107 'A'>;
 ('b' e.106) e.107 =  <F38 (e.106) e.107 'B'>;
 ('c' e.106) e.107 =  <F38 (e.106) e.107 'C'>;
 (s.108 e.106) e.107 =  <F38 (e.106) e.107 s.108>;
 () e.107 =  e.107;
}

****************************** The End ************************************
Приемлемый результат Рефала-5λ
  $ENTRY Test {
    e.X#1 = <F@1 e.X#1>;
  }
  
  F {
    (e.Acc#1) 'a' e.Rest#1 = <F (e.Acc#1 'A') e.Rest#1>;
    (e.Acc#1) 'b' e.Rest#1 = <F (e.Acc#1 'B') e.Rest#1>;
    (e.Acc#1) 'c' e.Rest#1 = <F (e.Acc#1 'C') e.Rest#1>;
    (e.Acc#1) s.1#1 e.Rest#1 = <F (e.Acc#1 s.1#1) e.Rest#1>;
    (e.Acc#1) = e.Acc#1;
  }
  
  Test@0 {
  }
  
  F@0 {
  }
  
  F@1 {
    'a' e.Rest#1 = <F ('A') e.Rest#1>;
    'b' e.Rest#1 = <F ('B') e.Rest#1>;
    'c' e.Rest#1 = <F ('C') e.Rest#1>;
    s.1#1 e.Rest#1 = <F (s.1#1) e.Rest#1>;
    /* empty */ = /* empty */;
    e.dyn#1 = <F@0 () e.dyn#1>;
  }
Красивый результат MSCP-A
$ENTRY Go {
 e.x1 =  <F_12345_0 () (e.x1)>;
}


F_12345_0 {
 (e.x1) ('a' e.x2) =  <F_12345_0 (e.x1 'A') (e.x2)>;
 (e.x1) ('b' e.x2) =  <F_12345_0 (e.x1 'B') (e.x2)>;
 (e.x1) ('c' e.x2) =  <F_12345_0 (e.x1 'C') (e.x2)>;
 (e.x1) (s.z1 e.x2) =  <F_12345_0 (e.x1 s.z1) (e.x2)>;
 (e.x1) () =  e.x1;
}

/* This file was generated by MSCP at Fri Nov 27 22:31:37 2020.*/

/* Elapsed time of embeddings is 15.0.*/

/* Elapsed time of generalizations is 0.0.*/

@TonitaN
Copy link
Member

TonitaN commented Nov 27, 2020

В последнем примере MSCP-A тупо ничего не сделал, в отличие от SCP4 :) А в первом примере злую шутку с SCP4 сыграла его особенность обобщать всё так, чтобы не было открытых переменных в обобщаемых выражениях. Имхо именно открытые переменные в обобщаемых выражениях позволили MSCP довольно адекватно свернуть программу.

@Mazdaywik
Copy link
Member Author

А во втором примере SCP4 перемудрил, выделяя выходные форматы. По-видимому, в первом тоже.

На самом деле, для меня во втором примере «тупо ничего не сделать» и есть приемлемый результат. В первом, в принципе, тоже.

Блин, почему же в первом примере авторазметка ни одной из функций не дала метку $DRIVE?


Я сейчас длинный текст пишу в #328. Жди, там должно быть интересно.

@Mazdaywik
Copy link
Member Author

@TonitaN, нашёл ошибку. Случайно в авторазметке запрещал прогонку всех функций. Исправил. Теперь надо заново тестировать. Может оказаться, что на максимальных настройках оптимизации всё опять не работает 😜, а заработало ранее из-за того, что одна из оптимизаций (прогонка) по факту была отключена.

Mazdaywik added a commit that referenced this issue Nov 27, 2020
Почему-то (видимо, невнимательный копипаст) все функции программы помещались
в список e.Forbidden, из-за чего они не прогонялись.
@Mazdaywik
Copy link
Member Author

Ещё примеры: #319 (comment).

@Mazdaywik
Copy link
Member Author

Почему коммит выше (dfa6e5a) оказался в этой заявке? Потому что он выявляет рассматриваемую проблему!

Компилятор при самоприменении задумывается и объедается по памяти (впрочем, это к #319). А делает он это из-за нашей любимой специализации по аккумулятору. Построен минимальный относительно небольшой пример, выявляющий ошибку:

Небольшой пример
*$FROM DisplayName
$EXTERN DisplayName;


$ENTRY InlineExpr {
  /* empty */ = '/* empty */';
  e.Expr = <InlineSubexpr e.Expr>;
}

AppendTerm {
  e.String '\'' ('\'' e.Term) = e.String e.Term;
  e.String ' ' (e.Term) = e.String ' ' e.Term;
  e.String (e.Term) = e.String ' ' e.Term;
}

InlineTerm {
  (Symbol e.Value) = <Symbol e.Value>;
  (TkVariable e.Value) = <TkVariable e.Value>;
  (Brackets e.Value) = <Brackets e.Value>;
  (CallBrackets e.Value) = <CallBrackets e.Value>;

  ';' = ';';
}

Symbol {
  Char s.Char = '\'' <CharRep s.Char> '\'';
  Number Cookie1 = '$COOKIE1';
  Number Cookie2 = '$COOKIE2';
  Number s.Number = <Symb s.Number>;
  Name e.Ident = '&' <DisplayName e.Ident>;
}

CharRep {
  '\'' = '\\\'';
  '\"' = '\\\"';
  '\\' = '\\\\';
  '\n' = '\\n';
  '\r' = '\\r';
  '\t' = '\\t';

  s.Char = s.Char;
}

HexDigit {
  s.Number
    , <First s.Number '0123456789abcdef'> : (e.1) s.Digit e.2
    = s.Digit
}

TkVariable {
  s.Type e.Index s.Depth = s.Type '.' e.Index '#' <Symb s.Depth>;
  s.Type e.Index = s.Type '.' e.Index;
}

Brackets {
  e.Expr = '(' <InlineSubexpr e.Expr> ')'
}

CallBrackets {
  (Symbol Name e.Function) /* пусто */ = '<' <DisplayName e.Function> '>';

  (Symbol Name e.Function) e.Expr
    = '<' <DisplayName e.Function> ' ' <InlineSubexpr e.Expr> '>';

  (TkVariable e.Indirect) e.Expr
    = '<' <TkVariable e.Indirect> ' ' <InlineSubexpr e.Expr> '>';

  e.Expr = '< ' <InlineSubexpr e.Expr> '>';
}

InlineSubexpr {
  t.Term e.Expr = <DoInlineExpr (<InlineTerm t.Term>) e.Expr>;

  /* пусто */ = /* пусто */;
}

DoInlineExpr {
  (e.Text) t.NextTerm e.Expr
    = <DoInlineExpr (<AppendTerm e.Text (<InlineTerm t.NextTerm>)>) e.Expr>;

  (e.Text) = e.Text;
}

Наблюдаются следующие истории сигнатур:

Fri Dec 04 21:53:54 2020: History of DoInlineExpr@635
    DoInlineExpr@2: ['\'\\\"\'']
    DoInlineExpr@25: ['\'' e.0#0 '\\\"\'']
    DoInlineExpr@76: ['\'' e.0#0]
    DoInlineExpr@235: ['\\\\\'']
    DoInlineExpr@448: ['\\\\' e.0#0 '\'']
    DoInlineExpr@635: ['\\\\' e.0#0 s.1#0]

Fri Dec 04 21:53:54 2020: History of DoInlineExpr@636
    DoInlineExpr@2: ['\'\\\"\'']
    DoInlineExpr@25: ['\'' e.0#0 '\\\"\'']
    DoInlineExpr@76: ['\'' e.0#0]
    DoInlineExpr@235: ['\\\\\'']
    DoInlineExpr@448: ['\\\\' e.0#0 '\'']
    DoInlineExpr@636: ['\\\\' e.0#0 '.' e.1#0]

Fri Dec 04 21:53:54 2020: History of DoInlineExpr@637
    DoInlineExpr@2: ['\'\\\"\'']
    DoInlineExpr@25: ['\'' e.0#0 '\\\"\'']
    DoInlineExpr@76: ['\'' e.0#0]
    DoInlineExpr@235: ['\\\\\'']
    DoInlineExpr@448: ['\\\\' e.0#0 '\'']
    DoInlineExpr@637: ['\\\\' e.0#0]
…
Fri Dec 04 21:53:54 2020: History of DoInlineExpr@654
    DoInlineExpr@8: ['$COOKIE1']
    DoInlineExpr@48: [e.0#0 '$COOKIE1']
    DoInlineExpr@96: [e.0#0 '$COOKIE' s.1#0]
    DoInlineExpr@271: [e.0#0 '$COOKIE\\\'\'']
    DoInlineExpr@476: [e.0#0 s.1#0 s.2#0 s.3#0 s.4#0 s.5#0 s.6#0 s.7#0 s.8#0 s.9#0 s.10#0]
    DoInlineExpr@654: [e.0#0 s.1#0 s.2#0 s.3#0 s.4#0 s.5#0 s.6#0 s.7#0 s.8#0 s.9#0 ' \'\\\"\'']
…
Fri Dec 04 21:53:54 2020: History of DoInlineExpr@738
    DoInlineExpr@8: ['$COOKIE1']
    DoInlineExpr@48: [e.0#0 '$COOKIE1']
    DoInlineExpr@96: [e.0#0 '$COOKIE' s.1#0]
    DoInlineExpr@286: [e.0#0 '$COOKIE ;']
    DoInlineExpr@518: [e.0#0 s.1#0 s.2#0 s.3#0 'O' s.4#0 s.5#0 s.6#0 s.7#0 s.8#0]
    DoInlineExpr@738: [e.0#0 s.1#0 s.2#0 s.3#0 'O' s.4#0 s.5#0 s.6#0 s.7#0 ' \'\\n\'']
…

Забавно появление 'O' в ряду s-параметров. По ходу дела, они возникают как обобщение e._ '$COOKIE;' и e._ '$COOKIE;' s._ — при наложении одного на другое сохраняется только повторяющаяся буква 'O'.

Конечно, куки — зло, и их мы обязательно удалим (#284), но это делу мало поможет.

Для решения данной проблемы в рамках #319 аккумулятор будет принудительно обобщаться. В рамках текущей задачи предстоит придумать способ борьбы с этим раздуванием аккумулятора, дабы обобщать вручную не требовалось.

@Mazdaywik
Copy link
Member Author

Mazdaywik commented Mar 27, 2021

Ослабление свистка

Свисток Хигмана-Крускала, конечно, является хорошим предпорядком, но в прямой реализации порой свистит очень поздно. Это может быть оправдано в суперкомпиляторе, где требуется проводить более точный анализ программы, ведь суперкомпилятор — это прежде всего инструмент для исследования программ. Но для оптимизирующего компилятора высокая точность в ущерб времени работы и размеру создаваемой программы может быть неадекватной.

Далее будем обозначать <F E′> — верхний узел графа (из истории) и <F E″> — нижний узел графа (текущий).

Поэтому стоит рассмотреть другие свистки:

  1. Упрощающее отношение из SCP4. Фактически, это вариант свистка Хигмана-Крускала, в котором вообще не учитываются какие-либо e-переменные.

  2. |E′||E″|, где |•| — длина выражения в токенах (символ, переменная — 1, скобки — 2) или в конструкторах (символ, переменная, пара скобок — 1). Очевидно, хороший предпорядок. И, не менее очевидно, чертовки слабый. Никак не учитывает внутреннюю структуру выражений E′ и E″.

  3. BAG[E′]BAG[E″], где BAG[•] строит мультимножество токенов BAG : Tok → ℕ. Тоже является хорошим предпорядком, мешки тегов использовались в суперкомпиляторе М. Болингброка и С. П. Джоунса (см. Improving supercompilation: tag-bags, rollback, speculation, normalisation, and generalisation). Токены нормируются — у переменных стираются индексы, у символов-чисел стираются значения. Умозрительно оценить качество свистка затрудняюсь, надо экспериментировать.

  4. (E*, S′, S″) = MSG[E′, E″], E*//S′ = E′, E*//S″ = E″, S′ = { E′i ← vi | i ∈ 1…n }, S″ = { E″i ← vi | i ∈ 1…n }, ∀i ∈ 1…n . |E′i] ≤ |E″i|. Иначе говоря, берётся наиболее тесное обобщение (в Рефале-5λ будет ГСО), для соответствующих переменных сводящих подстановок выполняется сравнение длин в токенах (или в конструкторах), свисток свистит, когда все подстановки не меньше по длине.

    Чем он интересен:

    • Он, определённо, точнее свистка № 2, оперирующего длиной самого аргумента.
    • При определении свистка сразу же строится и обобщение двух выражений.
    • Свисток учитывает содержательные свойства рефал-программ, а именно, форматы функций. Если у функции есть формат, то ГСО для пары выражений или будет самим форматом, или его уточнением, переменные ГСО будут параметрами функции. Если функция на каждой итерации уменьшает один из своих аргументов, то в этом случае она гарантированно завершается, а значит, вычисляется на этапе трансляции — свистеть не надо. И этот свисток свистеть не будет.
    • Недостатком свистка могут быть высокие вычислительные затраты: нужно вычислять ГСО и подстановки. Однако, ГСО вычисляется как коллапс обобщения изображений образцов, изображения в истории можно кэшировать. При вычислении свистка участвуют только два образца, поэтому можно подобрать специальный алгоритм. Можно вместо ГСО строить обобщение по алгоритму, используемому в SCP4, которое вычисляется более эффективно. Этот алгоритм строит ЛСО в нашей терминологии, ГСО не всегда достигается. Можно придумать алгоритм, который одновременно вычисляет обобщение и сводящие подстановки.
  5. Комбинация № 4 и № 3: вычисляем обобщение, для сводящих подстановок требуем вложение мультимножеств: ∀i ∈ 1…n . BAG[E′i] ⊆ BAG[E″i]. Слишком сложен. Скорее всего, неоправданно сложен.

@Mazdaywik
Copy link
Member Author

@TonitaN, есть что прокомментировать?

@Mazdaywik
Copy link
Member Author

И ещё один свисток. Отношение Хигмана. Аргументы функций рассматриваем как строки токенов, просто стираем токены. В отличие от отношения Хигмана-Крускала, это отношение можно проверить за линейное время.

@TonitaN
Copy link
Member

TonitaN commented Apr 7, 2021

@Mazdaywik
Copy link
Member Author

Нет, не видел. Почитаю. Спасибо.

@Mazdaywik
Copy link
Member Author

@TonitaN, прочитал статью. Спасибо!

Некоторые WQO более различающие (discriminative) чем другие, причём бывают строго более различающие. Например, отношение Хигмана-Крускала будет строго более различающим, чем отношение Хигмана на строках, описывающих объектные выражения как цепочки токенов. Но отношение Хигмана вычисляется быстрее. А это значит, что для ускорения анализа можно сначала проверять отношение Хигмана, а потом (если оно сработало) — более дорогое отношение Хигмана-Крускала. В статье есть схемы, как WQO относятся между собой по различающей силе.

Вообще, надо исследовать разные варианты, и посмотреть, какой приемлемее для компилятора.

Важно, чтобы

  • (а) предсказуемо оптимизировались распространённые идиомы (например, условие с функцией OneOf),
  • (б) можно было писать простые интерпретаторы DSL’ей, специализация которых завершается из-за того, что оптимизируемая программа на рекурсивных вызовах убывает, либо дословно повторяет предыдущие конфигурации (например),
  • (в) при попытке оптимизировать всё компилятор не увязал.

По-моему, оптимальным вариантом был бы вариант № 4 — вычислить MSG и сравнить длины подстановок в токенах.

@Mazdaywik
Copy link
Member Author

Скопирую отсюда: #359 (comment)

Возможно, стоит ослабить отношение Хигмана-Крускала:

    ResolveIncludes@1: [t.0 () (t.1 s.2 ((e.3) (e.3)) () e.4)]
    ResolveIncludes-CheckAlias@2: [t.0 () () ((t.1 s.2 ((e.3) (e.3)) (e.4 (Include t.5 e.6)) e.7)) () (e.6)]
    ResolveIncludes-CheckValid@3: [t.0 () () ((t.1 s.2 ((e.3) (e.3)) (e.4 (Include t.5 e.6)) e.7)) () (e.6) e.8]
    ResolveIncludes@5: [t.0 ((e.1)) (e.2 s.3 ((e.4) (e.4)) (e.5 (Include t.6 e.7)) e.8)]

Сопоставим @1 и @5:

    ResolveIncludes@1: [t.0 (     ) (t.1 s.2 ((e.3) (e.3)) (                     ) e.4)]
    ResolveIncludes@5: [t.0 ((e.1)) (e.2 s.3 ((e.4) (e.4)) (e.5 (Include t.6 e.7)) e.8)]

Если разрешить e-переменной поглощать другие переменные, то здесь произошло бы вложение.

Mazdaywik added a commit that referenced this issue Jul 24, 2021
По-нормальному, нужно каждый случай обобщения описать, сделать выводы
и решить проблему в общем случае в рамках #332. А пока я анализировал
лог и подсекал наиболее явные точки распухания.
Mazdaywik added a commit that referenced this issue Jul 30, 2021
По-нормальному, нужно каждый случай обобщения описать, сделать выводы
и решить проблему в общем случае в рамках #332. А пока я анализировал
лог и подсекал наиболее явные точки распухания.
Mazdaywik added a commit that referenced this issue Jul 31, 2021
По-нормальному, нужно каждый случай обобщения описать, сделать выводы
и решить проблему в общем случае в рамках #332. А пока я анализировал
лог и подсекал наиболее явные точки распухания.
@Mazdaywik
Copy link
Member Author

В комментарии #359 (comment) я написал:

Часто формируются сигнатуры, содержащие «открытые переменные» e.1 … e.2, и эти сигнатуры, как правило, довольно бессодержательны.

    Block@10: [t.0 Classic (TkOpenBlock t.1) e.2 (e.3 (Symbol Number Cookie1) (Symbol Number Cookie2)) e.4]
    Block@11: [t.0 s.1 (TkOpenBlock t.2) e.3 (e.4 (Symbol Number Cookie1) (Symbol Number Cookie2)) e.5]
    Block@12: [t.0 s.1 (TkOpenBlock t.2) e.3 (e.4) e.5]
    Block@13: [t.0 s.1 (TkOpenBlock t.2) e.3 (e.4 (Brackets e.5 e.6)) e.7]
    Block@14: [t.0 s.1 (TkOpenBlock (Brackets e.2 e.3)) e.4]
    Block@15: [t.0 s.1 (TkOpenBlock t.2) e.3 (e.4 (CallBrackets e.5 e.6)) (s.7 t.8 e.9) e.10]
    Block@16: [t.0 s.1 (TkOpenBlock (CallBrackets e.2 e.3)) (s.4 t.5 e.6) e.7]
    Block@17: [t.0 s.1 e.2 s.3 (e.4) e.5]
    Block@18: [t.0 s.1 (TkOpenBlock t.2) e.3 (s.4 t.5 e.6) e.7]
    Block@19: [t.0 s.1 (TkOpenBlock t.2) e.3 (e.4 (Brackets e.5)) e.6]
    Block@1: [t.0 s.1 e.2]
    Block@20: [t.0 s.1 (TkOpenBlock (Brackets e.2)) e.3]
    Block@21: [t.0 s.1 (TkOpenBlock t.2) e.3 (e.4 (CallBrackets e.5)) (s.6 t.7 e.8) e.9]
    Block@22: [t.0 s.1 (TkOpenBlock (CallBrackets e.2)) (s.3 t.4 e.5) e.6]
    Block@23: [t.0 s.1 (TkOpenBlock t.2) e.3 (e.4 (Brackets (Symbol Name t.5 e.6) e.7)) e.8]
    Block@2: [t.0 s.1 (TkOpenBlock t.2) e.3]
    Block@3: [t.0 Extended e.1]
    Block@4: [t.0 Extended (TkOpenBlock t.1) e.2]
    Block@5: [t.0 Classic (TkOpenBlock t.1) e.2 e.3]
    Block@6: [t.0 Classic (TkOpenBlock t.1) e.2]
    Block@7: [t.0 Extended (TkOpenBlock t.1) e.2 (e.3) e.4]
    Block@8: [t.0 Extended (TkOpenBlock t.1) e.2 e.3]
    Block@9: [t.0 s.1 (TkOpenBlock t.2) e.3 e.4]
    Expression@0: [t.STAT0 s.STAT0 s.STAT1 e.STAT0]
    Expression@10: [t.0 s.1 Pattern e.2 s.3 (e.4 e.5) e.6]
    Expression@11: [t.0 Classic Pattern e.1 e.2]
    Expression@12: [t.0 Classic Pattern e.1]
    Expression@13: [t.0 Extended Pattern e.1 (e.2) e.3]
    Expression@14: [t.0 Extended Pattern e.1 e.2]
    Expression@15: [t.0 s.1 Pattern e.2 e.3]
    Expression@16: [t.0 Classic Pattern e.1 (e.2 (Symbol Number Cookie1) (Symbol Number Cookie2)) e.3]
    Expression@17: [t.0 s.1 Pattern e.2 (e.3 (Symbol Number Cookie1) (Symbol Number Cookie2)) e.4]
    Expression@18: [t.0 s.1 Pattern e.2 (e.3) e.4]
    Expression@19: [t.0 s.1 Pattern e.2 (e.3 (Brackets e.4 e.5)) e.6]
    …

Поэтому предлагается участки между двумя e-параметрами обобщать. Фактически, сигнатура тогда будет сводиться к жёсткому выражению с возможными кратными вхождениями параметров. Конечно, это потеря информации, но у нас не суперкомпилятор, а просто компилятор.

Проблема в том, что благодаря #322 и #251 компилятор стал слишком умным. И там, где старый оптимизатор не был ни на что способен, этот начинает мудрить.

В частности, одной из причин мудрения является неизвестность форматов функций компилятору. Рассмотрим пример:

/*
  <G t.X t.A> == t.X
*/
G {
  …
}

/*
  <F t.X t.Y t.Z e.Items> == …
*/
F {
  t.X t.Y t.Z t.Next e.Rest
    = <F <G t.X t.Next> t.Y t.Z e.Rest>;

  t.X t.Y t.Z /* пусто */ = t.X t.Y t.Z;
}

Функция G принимает и возвращает один терм — это знает программист, но не знает компилятор. При специализации рекурсивного вызова F сигнатура получится <F e.Call t.Y t.Z e.Rest><F e.1 t.2 t.3 e.4>. При сопоставлении с первым образцом получатся следующие сужения:

  • e.Call → t.1 t.2 t.3 t.4 e.5
  • e.Call → t.1 t.2 t.3
  • e.Call → t.1 t.2
  • e.Call → t.1
  • e.Call → ε
    Экземпляр примет вид:
F@1 {
  (t.1 t.2 t.3 t.4 e.5) t.6 t.7 e.8 = <F <G t.1 t.4> t.2 t.3 e.5 t.6 t.7 e.8>;
  (t.1 t.2 t.3) t.6 t.7 e.8 = <F <G t.1 t.6> t.2 t.3 t.7 e.8>;
  (t.1 t.2) t.6 t.7 e.8 = <F <G t.1 t.7> t.2 t.6 e.8>;
  (t.1) t.6 t.7 t.8 e.9 = <F <G t.1 t.8> t.6 t.7 e.9>;
  () t.6 t.7 t.8 t.9 e.10 = <F <G t.6 t.9> t.7 t.8 e.10>
  (t.1) t.2 t.3 = t.1 t.2 t.3;
  () t.1 t.2 t.3 = t.1 t.2 t.3;
}

В первом предложении сформируется сигнатура <F e.1 t.2 t.3 e.4 t.5 t.6 e.7>, т.е. примет вид даже e.? … e.? … e.?.

В рассмотренном примере для всех новых сигнатур сработает условие остановки. Но в реальном коде, где первое предложение может быть сложнее:

F {
  t.X t.Y t.Z (A e.A) e.Rest
    = <F <G t.X e.A> t.Y t.Z e.Rest>;

  t.X t.Y t.Z (B e.B) e.Rest
    = <F <G t.Y e.B> t.Y t.Z e.Rest>;
…
}

до остановки может пройти довольно много итераций.

Возможным решением может быть вынесение вызова G в присваивание:

  t.X t.Y t.Z t.Next e.Rest
    = <G t.X t.Next> : t.X^
    = <F t.X t.Y t.Z e.Rest>;

но это не всегда целесообразно.

Предложенное в комментарии #359 (comment) поведение в некоторых случаях специализирует хуже, чем использование явно заданных форматов. Если при использовании формата внутри статического параметра было … e.? … e.? …, то это выражение как есть попадёт в экземпляр. В предлагаемом варианте не попадёт.

Что делать? Возможно, более целесообразным был бы специальный алгоритм обобщённого сопоставления для специализации. Решение клэшей выглядели бы так:

  • Клэши вида T : Pt решаются также, как и раньше.
  • Отделения термов T E : Pt P и E T : P Pt решаются также, как и раньше.
  • Клэши вида E : e.X, очевидно, преобразуются в присваивания.
  • Клэши вида e.X : P решаются как e.X → P′, vars(P′) ← vars(P), где P′ идентичен P, однако, все переменные в нём свежие. Это правило предлагалось как частный случай в Тестирование, рефакторинги, оптимизации и исправления после ВКР #359 (comment) в качестве оптимизации, но здесь он не частный случай, а основной.
  • Остальные клэши неразрешимы — обобщаются.
  • Симметричные клэши решаются также, как и раньше.

Предложенный алгоритм, очевидно, слабее реализованного, однако, у него два преимущества:

  • он оптимизировал бы все те случаи, которые ранее оптимизировались с явно заданными форматами,
  • в экземплярах предложений было бы не больше, чем в исходных функциях, предложения при специализации не размножались бы (примечание: аварийные предложения не считаются),
  • специализация имела бы черты прогонки — при специализации интерпретаторов не требовалось бы разбивать функцию на базисную специализируемую и внутреннюю прогоняемую (например).
  • не было бы рассмотренных выше бессодержательных сигнатур.

Но я ничего менять в алгоритме обобщённого сопоставления не буду, вместо этого сделаю принудительное обобщение участков e.? … e.?. Возможно, в будущем что-то переделаю.

Mazdaywik added a commit that referenced this issue Aug 1, 2021
По-нормальному, нужно каждый случай обобщения описать, сделать выводы
и решить проблему в общем случае в рамках #332. А пока я анализировал
лог и подсекал наиболее явные точки распухания.

В Log-AST.ref был интересный случай квадратичного распухания,
а в остальных местах — типичная специализация по аккумулятору.
@Mazdaywik
Copy link
Member Author

Mazdaywik commented Aug 18, 2021

Перестройка сверху при специализации и слишком большие деревья

Про перестройки сверху и снизу

В суперкомпиляции рассматривают два способа обобщения при обнаружении «опасного сходства» вдоль некоторой ветви — перестройка сверху и перестройка снизу. В обоих случаях строится обобщение двух конфигураций, которое затем развивается. Разница между ними в том, что при перестройке сверху строится стрелка от верхней конфигурации к обобщённой, граф, построенный ранее из верхней конфигурации, отбрасывается. При перестройке снизу ничего не отбрасывается, стрелка к обобщённой строится от нижней конфигурации.

Оба подхода одинаково гарантируют завершение суперкомпиляции и построение конечного графа.

Дальше я дам два утверждения про перестройки сверху и снизу, без доказательств, но интуитивно мне они кажутся верными. Пусть у нас есть два суперкомпилятора: SCP↑, осуществляющий перестройку сверху, и SCP↓, осуществляющий перестройку снизу. Пусть у нас есть некоторая (произвольная) программа P. Обозначим:

Перестройки сверху Перестройки снизу Пояснение
G↑ = SCP↑(P) G↓ = SCP↓(P) граф суперкомпиляции, построенный данным суперкомпилятором
SC↑ = SETCONF(G↑) SC↓ = SETCONF(G↓) множество конфигураций, присутствующих в графе суперкомпиляции
SCR↑ = SETCONF_RUNTIME[ SCP↑(P) ] SCR↓ = SETCONF_RUNTIME[ SCP↓(P) ] множество конфигураций, которые появляются в процессе суперкомпиляции
  • Очевидно, что SC↓ = SCR↓, т.к. при перестройках снизу все конфигурации в графе сохраняются.
  • Очевидно, что SC↑ ⊆ SCR↑, т.к. при перестройках сверху часть построенного графа иногда отбрасывается, а значит, часть конфигураций, появившихся во время выполнения, в финальный граф попасть не могут.

А вот неочевидные суждения, к которым у меня доказательств нет, но которым я интуитивно верю.

  • SC↑ ⊆ SC↓, — граф, построенный суперкомпиляцией с перестройкой снизу, содержит все конфигурации, из которых состоит граф, построенный перестройкой сверху.
  • SCR↑ ⊆ SC↓ — граф, построенный суперкомпиляцией с перестройкой снизу, содержит все конфигурации, которые возникали в процессе суперкомпиляции с перестройкой сверху.

@TonitaN, ты как думаешь?

Будем считать оба суждения истинными (upd: по-видимому, они не верны — #332 (comment)). В этом случае программа, построенная из G↓ будет содержать те же рекурсивные функции, что и G↑, но помимо них в ней будут лишние развёрнутые витки циклов, а также некоторые циклы для некоторых частных случаев. Т.е. на лицо то самое распухание, которому посвящена настоящая заявка.

Перестройки сверху и снизу при специализации

В актуальной реализации при специализации используется перестройка снизу. Причина — необходимость в ограничении времени работы некоторым разумным пределом. Компилятор не суперкомпилятор. Все суперкомпиляторы на данный момент исследовательские (других не знаю), их цель — получить интересную остаточную программу для интересной входной программы. Цель компилятора — получить работающую программу на целевом языке, эквивалентную программе на исходном. Цель оптимизирующего компилятора — получить более эффективную программу, чем неоптимизированную, но при этом за разумное время. А для суперкомпиляции время работы в принципе не предсказуемо.

Поэтому в актуальной реализации (до #340) был принят следующий подход: оптимизация выполняется по проходам, на каждом проходе выполняется небольшой конечный объём работы. Проходы выполняются до тех пор, пока либо программа не перестанет меняться, либо пока не исчерпается счётчик проходов. На проходах прогонки за каждый проход прогоняется ровно по одному вызову в каждом предложении. На проходе специализации строятся экземпляры для вызовов, но вызовы внутри экземпляров не оптимизируются (даже если используется специализация без прогонки).

В #340 @Apakhov реализовал «ациклическую суперкомпиляцию», в ходе которой для каждого результатного выражения строится ациклический граф, который сводится к конечному набору сужений. Хотя используемый алгоритм суперкомпиляции всегда завершается, построение графа может требовать много времени. Для ограничения времени работы в код зашиты максимальная глубина дерева и максимальное количество вершин. Но об этом позже.

Преимуществом перестройки снизу является то, что не требуется выполнять откатов — отбрасывать ранее построенные части графа. В терминах компилятора — не требуется стирать из дерева ставшие ненужными экземпляры и переписывать те функции, где они появились. На каждом проходе можно для новых вызовов добавлять новые экземпляры, не меняя ранее построенного синтаксического дерева.

Недостаток перестройки снизу — распухание дерева, которому посвящена настоящая заявка.

Для борьбы с распуханием предлагается использовать перестройку сверху при специализации. Принцип построения — примерно тот же, что и при суперкомпиляции, в частности, в #340.

Рассмотрим два случая: сначала случай специализации неспециализируемой функции, а потом специализируемой.

Пусть у нас есть некоторая функция F.

  • Инициализируем пустую историю.
  • Выполняем в ней все прогонки, согласно имеющемуся алгоритму прогонки. Хвостовые функции G*n для не до конца прогнанных функций не строим, см. Удалять и не порождать неиспользуемые функции #228.
  • Затем, выполняем в ней специализации всех вызовов специализируемых функций. Однако, построение экземпляров откладываем, а просто запоминаем новые сигнатуры.
  • Затем, для первой новой сигнатуры строим экземпляр. Т.к. история пустая, на этом шаге обобщений быть не может.
  • В строящемся графе рисуем стрелки от сигнатуры функции F к новым сигнатурам G@i.
  • Выбираем из сигнатур самую первую. Строим для неё экземпляр.
  • Выполняем над ним оптимизацию прогонки также, как и для F (т.е. без построения H*n). Выполняем в нём специализацию вызовов, в истории будет находиться сигнатура для G@i. Если обобщений не произошло, получим несколько новых сигнатур-потомков, которые аналогичным образом добавляем в граф.
  • Когда от очередного экземпляра построено законченное дерево, переходим к «братской» сигнатуре и строим экземпляр для неё. В общем, экземпляры порождаются лениво.
  • Может случиться чудо, что таким образом построится конечное дерево из экземпляров без необходимости в обобщениях. Листьями дерева будут экземпляры, при специализации вызовов в которых новых экземпляров не построилось. Оптимизированную функцию F и все построенные экземпляры добавляем к синтаксическому дереву единицы трансляции.
  • Но чуда может не случиться — сигнатура очередного вызова (обозначим её G@j) может оказаться опасно похожей на одну из сигнатур в истории (обозначим её G@i). Что делать?
  • В этом случае вычисляем обобщение между G@i и G@j, назовём его G@k = GEN(C@i, C@j). Далее нужно выполнить перестройку сверху:
    • восстанавливаем таблицу сигнатур на момент до построения экземпляра G@i,
    • в таблице для сигнатуры G@i вместо имени экземпляра помещаем ссылку на сигнатуру G@k (т.е. в таблице два вида записей — конкретные записи и символьные ссылки на другие сигнатуры-обобщения),
    • чтобы не переписывать предка G@i, заменяем тело G@i следующим телом (назовём такой экземпляр фиктивным):
      G@i {
        arg = <G@k arg′>;
      }
      
      (построение arg и arg′ дело техники, описывать его не интересно), фиктивный экземпляр встроится последним проходом прогонки экземпляров.
    • если экземпляра для сигнатуры G@k ещё не было, то строим его и оптимизируем с последующим построением дерева.
  • Важно упомянуть случай, когда обобщение C@k = GEN(C@i, C@j) ≡ C@i. Это случай зацикливания, когда обобщать не нужно, а нужно вызвать из дочернего узла родительский. В этом случае тоже можно построить фиктивный экземпляр
    G@j {
      arg = <G@i arg′>;
    }
    
    Либо, можно такие случаи распознавать в процессе специализации и вместо построения фиктивного экземпляра, сразу заменять вызов G на вызов G@i с правильным аргументом.
  • Когда дерево построено, удаляем из таблицы сигнатур все сигнатуры-ссылки. Для оптимизации других функций они, скорее всего, будут лишними (хотя не уверен).

Случай оптимизации специализируемой функции отличается лишь первым пунктом:

  • Инициализируем историю фиктивной сигнатурой F@0 с ГСО предложений функции F.
  • Дальше аналогично.

Возможно, алгоритм не оптимален, но работать вроде будет.

Ограничение специализации по времени

В описанном виде алгоритм неприемлем, т.к. может работать неопределённо долго. Предлагается ввести ограничения на глубину дерева и число узлов (новых нефиктивных экземпляров) аналогично #340.

Допустим, мы стали строить дерево, и сработал один из лимитов: или получилась очень длинная ветвь, или построилось очень много новых не-фиктивных экземпляров. Что дальше делать? Можно предложить два варианта:

  • cбросить в программу незавершённое дерево (аналогично сделано при прогонке в Прогонка, которая не зацикливается #340), непросчитанные сигнатуры просчитать, выполнить в них прогонку без специализации,
  • отказаться от специализации функции F, поместить в программу только результат прогонки без специализации.

Недостаток первого варианта в том, что программа, очевидно, распухает. Ведь возможно, что если бы лимит был чуть побольше, то сработало бы условие остановки и дерево свернулось и стало бы более компактным, а вместо этого в остаточную программу вывалился немеряно распухший полуфабрикат.

Недостаток второго варианта в том, что функция остаётся недооптимизированной. Не исключено, что причиной раздутого дерева является какая-то сигнатура в середине дерева, и если её не трогать, то оставшееся дерево получилось бы замкнутым и небольшим.

Но вот в чём вопрос: как найти такую «проблемную» сигнатуру (или сигнатурЫ, если их несколько)? Задача, по всей видимости, разрешима только полным перебором: рассмотреть каждое подмножество из 2N всех подмножеств сигнатур и построить дерево, где выбранные сигнатуры не специализируются, как не специализируются тривиальные сигнатуры. Если подмножество сигнатур выбрано «правильно», то построенное дерево не будет выходить за лимиты. Но только как из «правильных» подмножеств выбрать подходящее? По размеру нельзя, т.к. минимальным в этом случае будет подмножество из единственной корневой сигнатуры. По размеру + (средней или минимальной) глубине? Тоже не очевидно, что такая эвристика работает. Но в любом случае полный перебор из 2N неприемлем на практике.

Так что крайние варианты (отказываться специализировать корень и отказываться специализировать тот лист, на котором достигнут один из лимитов) имеют явные недостатки, а промежуточный вариант не очевиден.

Поиск ответа на вопрос., что делать с превышением лимитов, важен не только для специализации, но и для прогонки, т.к. ответ на вопрос может быть приемлем и для ациклической суперкомпиляции.

Mazdaywik added a commit that referenced this issue Aug 19, 2021
Предыдущий коммит опять углубил возможности оптимизации, из-за чего
пришлось выявлять и купировать очередные распухания.
Mazdaywik added a commit that referenced this issue Aug 19, 2021
Проблема возникала на этапе прогонки экземпляров: на первой стадии
оптимизации функция gen_e__ оберегала от взрывной прогонки,
но на второй фазе получаются прогоняемые экземпляры gen_e__@n,
которые уже ничего не защищают. И программа распухает на финальной
стадии.
Mazdaywik added a commit that referenced this issue Aug 20, 2021
Распухание происходило из-за прогонки функции Inc. Решение: заменить
это определение функции на пометку $INTRINSIC.

Но Простой Рефал не поддерживает ключевое слово $INTRINSIC, поэтому
автотест пришлось сначала перевести на Рефал-5λ двумя предыдущими
коммитами.
Mazdaywik added a commit that referenced this issue Aug 22, 2021
По-нормальному, нужно каждый случай обобщения описать, сделать выводы
и решить проблему в общем случае в рамках #332. А пока я анализировал
лог и подсекал наиболее явные точки распухания.
@TonitaN
Copy link
Member

TonitaN commented Aug 24, 2021

Копипэйста из #362

...кстати, неочевидные утверждения из него пока что мне не очевидны. Для дерева (бесконечного) они, понятное дело, верны. А вот что касается графа --- сходу нет интуиции, что при перестройке снизу одна из конфигураций после обобщения не может обобщиться с какой-то из тех, которые находятся на отрезке от верхней (не обобщённой) до нижней конфигурации. Тогда уже может пойти развитие совсем другой истории.

@Mazdaywik
Copy link
Member Author

Mazdaywik commented Aug 24, 2021

Мне тоже они не очевидны, но почему-то мне кажется, что кусок истории между ними влиять не будет.

Допустим, у нас есть цепочка конфигураций: C1 → … → C2 → … → C3. И конфигурация C3 опасно похожа на C1. Суперкомпилятор SCP↓ построит обобщение C4 = MSG(C1, C3) и поместит его вниз:

C1 → … → C2 → … → C3 → C4

Конфигурация C4 будет более общей, чем C1. Какие-то части конфигурации, которые были выражениями в C1, будут переменными в C4. И из более общей C4 не должна получиться конфигурация более детальная, чем C2, которая в серединке (я рассматриваю отношение остановки Хигмана-Крускала). Примерно такие интуитивные соображения.

Хотя, наверное, можно построить контрпример. Что по пути из более детальной C1 отсекалась какая-то ветка, которая давала конфигурацию (назовём C5), похожую на C2. Но из более общей C4 эта ветка не отсечётся и произойдёт зацикливание с C2:

C1 → … → C2 → … → C3 → C4 → … → C5 → C6 = MSG(C2, C5)

В случае перестройки сверху тут будет C1 → C4 → … → C5, и C5 не будет обобщаться с C2, т.к. C2 в графе нет. В SCP↓ будет развиваться обобщённый C6 = MSG(C2, C5) подграф, в SCP↑ — более конкретный C5.

Так что, по-видимому, эти утверждения не верны. Чтобы было не «по-видимому», нужно построить контрпример для этой схемы.


А вообще, эти утверждения можно проверить экспериментально.

Известно два простых суперкомпилятора:

  • Суперкомпилятор Абрамова для модельного языка TSG. Он осуществляет перестройку снизу и «гиперцикл». Транзитные вершины не строит.
    Про гиперцикл После одного прохода суперкомпиляции выбираются все базисные вершины и для каждой из них выполняется суперкомпиляция. Получаются новые графы с новыми базисными вершинами. За несколько таких «гиперциклов», по словам Абрамова, множество базисных конфигураций перестаёт изменяться. Смысл в том, что у базисных конфигураций теперь стёрта история, и они развиваются «чище».
  • Суперкомпилятор Романенко (возможно, участвовал и Анд. Климов) для языка SLL. Осуществляет перестройку сверху. Транзитные вершины строит, делает локально-глобальный анализ.

Оба суперкомпилятора с открытыми исходниками. Можно дать курсовую или диплом: форкнуть один из этих суперкомпиляторов, поменять в нём вид перестройки и сравнить. А также проверить мои утверждения.

Mazdaywik added a commit that referenced this issue Sep 17, 2021
Ранее было обнаружено, что с новым алгоритмом специализации компилятор
в некоторых тестах увязает, и был добавлен грязный хак, ограничивающий
максимальное число экземпляров (54c28ca, #332).

Предыдущие два коммита стали строить дополнительные let-экземпляры, из-за
чего порог стал достигаться раньше и глубина оптимизации снизилась.
Поэтому потребовалось поднять порог.

Порог был поднят до 150 по двум причинам:

• В тесте saved-test-10_Mon-Aug-23-21-51-16-UTC-2021.ref (81667ba)
  на пороге в 100 экземпляров условие остановки не срабатывает
  (до let-экземпляров срабатывало). Опытным путём было выяснено,
  что условие установки срабатывает на пороге между 140 и 145, было
  выбрано круглое значение.

  Заметим, что этот тест в текущем коммите проходит.

• Тест saved-test-77_14.08.2021_20-00-01,67-big.ref (41ce59e, #314),
  напротив, проходит только благодаря наличию этого лимита (на момент
  написания коммита 41ce59e не анализировалась проблема
  с непрохождением теста, просто был повышен порог). С лимитом 200
  тест не проходит, с лимитом 150 проходит.
Mazdaywik added a commit that referenced this issue Sep 22, 2021
Теперь специализация одного вызова может порождать несколько экземпляров,
а значит, сравнение на точное значение может «перескочить».
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants