bmstu-iu9 / bmstu-iu9/refal-5-lambda

Древесные оптимизации нарушают семантику

Open
#276 8 comments 0 reactions 1 assignee Claimed by @Mazdaywik View on GitHub
bug
Dominant language
C++
Stars
97
Forks
40
PR merge metrics
No merged PRs in 30d

Description

Введение. Гарантии для равенства замыканий
===============================
Сравнение на равенство является фундаментальной операцией Рефала. Синтаксис образцов допускает кратные вхождения переменных, значения которых должны быть равны. Следовательно, в ядре языка должна быть определена операция сравнения на равенство для любых типов данных.

Одна из аксиом языка (если можно так выразиться) — значения, полученные путём копирования (кратные вхождения в результатное выражение) должны быть равны в смысле сопоставления с кратными переменными образца.

Например:
```Refal5
Eq {
t.Eq t.Eq = True;
t.X t.Y = False;
}

F {
e.X = ;
}
```
Функция `F` всегда должна возвращать `True`.

Ещё есть неявная аксиома — повторные s-переменные должны сопоставляться за константное время. В учебнике Турчина говорится, что открытые переменные, повторные t- и e-переменные скрывают за собой рекурсию, следовательно, виды сопоставлений рекурсию не скрывают, т.е. сопоставляются за константное время.

Рефал-5λ поддерживает вложенные функции в результатных выражениях. Во время выполнения для вложенных функций порождаются замыкания — значения типа «символ» (сопоставимые с s-переменными), которые можно вызывать как функции. Замыкания могут захватывать контекст.

Возникает вопрос: как сравниваются на равенство два замыкания? Для Простого Рефала в [`manul.pdf`](https://github.com/bmstu-iu9/refal-5-lambda/blob/a5b0b5bf6f8ddc451c6f5ef5be1129fa5befa196/doc/historical/manul.pdf) были определены следующие ограничения:

> 1. Экземпляры функций, как и другие атомы, копируются за константное время.
> 2. Передача управления на указатель на функцию выполняется за константное время.
Передача управления на замыкание может выполняться как за константное время, так и за время пропорциональное размеру контекста. Последнее возможно, если в поле зрения присутствует несколько копий данного замыкания, поэтому для вызова каждого из экземпляров требуется своя копия контекста — такова списковая реализация. 〈…〉
> 3. Два экземпляра функции, полученные путём копирования одного атома, равны.
> 4. Два замыкания, построенные из текстуально разных функциональных блоков, не равны.
> 5. Два замыкания, имеющие разное содержимое элементов контекста, не равны.
> 6. Указатели на функцию равны тогда и только тогда, когда они указывают на одну и ту же функцию.

_Примечание._ Следует уточнить понятие _текстуально разные._ Текстуально разными считаются не только блоки в разных позициях в одном файле, но и один и тот же блок в заголовочном файле, включённый в разные единицы трансляции.

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

```
F1 { = { = } }
F2 { e.X = { = e.X } }

Eq { 〈см. выше〉 }

Test {
e.X
= >>
>>
}
```
Функция `F1` создаёт замыкание с пустым контекстом, а для таких случаев вместо объекта замыкания создаётся просто указатель на неявную глобальную функцию (`&F1\1`). Поэтому первый вызов распечатает `True`. Так было сделано с самой первой реализации вложенных функций в `Simple Refal.004`.

Второй вызов по умолчанию распечатает `False`, поскольку два вызова `` создадут два объекта замыкания, которые сравнятся по ссылке. Но если функции `F2` и `Eq` прогнать (или даже встроить), то мы получим ``. Реализованный алгоритм обобщённого сопоставления с образцом допускает повторные переменные любого типа, причём сопоставление считается успешным, если значения этих переменных текстуально совпадают (если не совпадают и тип переменной не s — результат не определён). Поэтому здесь сопоставление будет успешным.

Далее, мы рассмотрим три примера. Один с мнимым нарушением семантики, два других — с реальным.

Воспроизведение ошибки
==================
Пример 1. Мнимое нарушение семантики при прогонке
-----------------------------------------------------------------
```Refal5
$ENTRY Go {
e.X
= > : False
= /* пусто */
}

$INLINE Clo, Eq;

Clo { e.X = { = e.X } }

Eq {
s.X s.X = True;
s.X s.Y = False;
}
```
Скачать: [closures-neq-drive.ref](https://github.com/bmstu-iu9/refal-5-lambda/files/4469142/closures-neq-drive.ref.txt).

Это случай неопределённого поведения, когда сравниваются два замыкания из одного и того же блока, с равными контекстами, но созданные в разное время.

Пример 2. Реальное нарушение семантики при прогонке
------------------------------------------------------------------
```Refal5
$ENTRY Go {
e.X
= > : True
= /* пусто */
}

$INLINE Dup;

Dup { e.X = e.X e.X }

Eq {
s.X s.X = True;
s.X s.Y = False;
}
```
Скачать: [closures-eq-drive.ref](https://github.com/bmstu-iu9/refal-5-lambda/files/4469117/closures-eq-drive.ref.txt).

Здесь встраивается вызов `Dup`, в результате чего `Eq` вызывается с аргументом
```Refal5

```
Во время выполнения создаются два одинаковых объекта замыкания. Но, поскольку замыкания сравниваются по ссылке, они оказываются не равны.

Пример 3. Реальное нарушение семантики при специализации
-------------------------------------------------------------------------
```Refal5
$ENTRY Go {
e.X = ;
}

$SPEC S s.STAT;

S {
s.X = : True = /* пусто */;
}

Eq {
s.X s.X = True;
s.X s.Y = False;
}
```
Скачать: [closures-neq-spec.ref.txt](https://github.com/bmstu-iu9/refal-5-lambda/files/4469160/closures-neq-spec.ref.txt).

Причина похожа на предыдущую — строятся два одинаковых экземпляра замыкания вместо копирования одного. Только теперь путём размножения значения статической переменной. Специализированная функция `S@1` выглядит так:
```Refal5
S@1 {
e.X#1 = >;

e.X#1 = ;
}
```

Решение
======
А вот однозначного решения тут пока не видно — везде есть компромиссы.
* Запретить сравнивать на равенство замыкания — останавливать аварийно программу при таком сравнении. Решение слишком контринтуитивное — программа будет падать «на ровном месте» с точки зрения пользователя. При написании повторных переменных придётся задумываться: а не могут ли там оказаться замыкания?
* Отказаться от принципа, что значения, созданные копированием, равны. Тоже контринтуитивное решение, да и прогонка повисает без серьёзных оснований.
* Усложнить прогонку и специализацию — придумать «фокусы», которые отслеживали бы равные ссылки во время преобразований.
* Сравнивать замыкания по значению. Это отказ от сравнения s-переменных за константное время. В принципе, отказ не такой страшный, поскольку для замыканий и вызов функции не выполняется за константу (копируется контекст).
* Вообще отказаться от символов-замыканий. Можно сделать замыкания t-переменными, скрестив их с абстрактными скобками. Решение красиво тем, что существенно упрощает и семантику языка, и реализацию компилятора. Недостаток — слишком большое изменение.

Приемлемыми мне видятся только последние три варианта. А может даже, последние два.

Буду думать летом после завершения #260, #256, #252, #253.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.