Комментарии к перегрузке оператора styx


Введение

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

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

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

Я прокомментирую и объясню путь, выбранный styx.

Примеры из других языков

Давайте посмотрим на синтаксис нескольких других систем перегрузки операторов. Я возьму системы, которые основаны, как и Styx, на функциях-членах, и которые позволяют перегружать вызовы.

struct S // D
{
    auto opCall(){}
    //   ^-----special identifier used to recognize the overload
}
Вход в полноэкранный режим Выход из полноэкранного режима
struct S // c++
{
    void operator()() const {}
    ///          ^-----special toks used to recognize the overload
}; 
Войти в полноэкранный режим Выход из полноэкранного режима

Мы можем выделить два стиля.

Стиль D основан на предопределенных идентификаторах. Это не на 100% удобное решение для пользователя, потому что вы должны помнить имя метода, но это четкая система, удобная для компилятора, потому что можно быстро найти, если что-то перегружено. Один поиск в таблице символов, а затем можно попробовать перегрузку.

Стиль C++ удобен для пользователя, () означает вызов, его легко запомнить. Однако компиляторная сторона этой системы не очень хороша. В частности, я бы критиковал тот факт, что для разбора перегрузок операторов требуется специальное правило грамматики.

Способ Стикса

Синтаксис перегрузки операторов в Styx не требует новой языковой конструкции и не основан на специальных идентификаторах. Он использует параметрический атрибут функции.

Атрибуты Styx имеют вид

Attribute ::= "@" Identifier ("("Expression ("," Expression)*")")?
Войти в полноэкранный режим Выйти из полноэкранного режима

Например, чтобы указать компилятору, должен ли он инлайнить функцию или нет.

@inline(true) function inlineMePlz();
@inline(false) function dontInlineMePlz();
Ввести полноэкранный режим Выйти из полноэкранного режима

Операторы повторно используют этот синтаксис, но, в отличие от C++, полное выражение, а не только символы, определяет, что функция должна перегружать, и это не требует специальных случаев в парсере.

struct S
{
    @operator(a(b)) function overloadCall();
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

@final class CallExpression : UnaryExpression
{
    @override function setOperator() { operator = leftParen; }   
}
Вход в полноэкранный режим Выход из полноэкранного режима

Семантика проверяется с помощью паттерна посетителя. Посетитель диспетчеризируется не с помощью динамических кастов, а с помощью оператора nodes. (для быстрого объяснения это связано с тем, что в компиляторе мы часто заботимся только о самом производном типе узла, а динамические приведения ищут идентичность во всем списке наследования, что излишне)

@inline function visitConcreteExpression(Expression* e)
{
    var Expression** ae = &e;
    switch e.operator do
    {
    on equal .. lesserEqual do visit( *(ae:CmpExpression**) );
    on plus         do visit( *(ae:AddExpression**)         );
    // etc...
    on leftParen    do visit( *(ae:CallExpression**)        );
    // etc....
    }
}

Вход в полноэкранный режим Выход из полноэкранного режима

А при посещении CallExpression достаточно проверить, содержит ли экземпляр агрегата, используемый в качестве параметра this, функцию, присоединенную к атрибуту @operator, которая имеет в качестве аргумента выражение с оператором leftParen.

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

/** Returns: `null` if operator overload is not possible and a CallExpression otherwise.
 * If the result is valid, its semantic is not yet run. */
function tryOperatorOverload(Expression* e; Expression*[] args): Expression*
{
    assert(args.length);
    assert(args[0].type, "call tryOperatorOverload after una.exp or bina.left sema");

    // only allowed on custom types or on pointer to custom types (DotExp implicit deref)
    var auto tp = args[0].type.asTypePointer();
    var auto ta = tp ? tp.modified.asTypeAggregate() else args[0].type.asTypeAggregate();
    if !ta do return null;

    // be sure that the aggregate `hasOpOvers` is defined
    if ta.declaration.progress != SemanticProgress.done do
        assert(0, "cannot try opover if aggregate declsema is not run");

    var auto ad  = ta.declaration;
    var auto ads = ad.symbol;

    if !ad.hasOpOvers do
        return null;

    // handle non-overridden opovers
    var Type*[+] chain;
    getInheritanceChain(ta, chain);
    chain ~= ta;

    foreach var auto t in chain.reverse do
    {
        var auto ts = asTypeAggregate(t).declaration.symbol;
        foreach var auto s in ts.children do
        {
            // on every member funcs and overloads ...
            var auto fd = symToFuncDecl(s);
            var auto od = symToOverDecl(s);
            var auto funOrOverDecl = fd ? od;
            if !funOrOverDecl do
                continue;

            // ... that are annotated @operator....
            var Attribute* oa;
            if (oa = funOrOverDecl.getAtOperator()) == null do
                continue;

            // dont check param count if func is an overload set
            var auto parametersMatch = true;
            if fd do
            {
                if fd.isStatic() do
                    parametersMatch = fd.parameters.length + 1 == args.length;
                else do
                    parametersMatch = fd.parameters.length == args.length;
            }

            foreach var auto p in oa.arguments do
            {
                var bool isMacroOp;
                if var auto oom = asOpOverloadMacro(p) do
                    isMacroOp = getMacroOperatorKind(e) == oom.kind;

                //... that handles the right operator...
                if (p.operator == e.operator || isMacroOp) && parametersMatch do
                {
                    var auto id = (new IdentExpression).create(args[0].startPos, funOrOverDecl.name,
                                   fd ? fd.returnType else od.asTypeDeclared, funOrOverDecl.symbol);

                    var auto de = (new DotExpression).create(args[0].startPos, args[0], id);
                    de.symbol   = funOrOverDecl.symbol;
                    de.type     = fd ? fd.returnType else od.asTypeDeclared;

                    var CallExpression* ce  = (new CallExpression).create(args[0].startPos, de);
                    ce.arguments            = args[1..$];

                    // ... rewrite as a CallExp
                    ce.isOpOverRewrite = true;
                    return ce;
                }
            }
        }
    }
    return null;
}  
Вход в полноэкранный режим Выйти из полноэкранного режима

Если процедура возвращает не нулевое значение, то перегрузка операторов сработала и семантика по умолчанию пропущена.

Опять же, использование операторов выражения делает обработку перегрузки операторов простой и очень общей, но не полностью…

Темная сторона перегрузки операторов styx

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

Составные выражения

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

Рассмотрим пример присваивания индексному выражению

a[0] = 1;
Войти в полноэкранный режим Выйти из полноэкранного режима

Определим структуру, которая перегружает два выражения, использованные в примере

struct A
{
    @operator(a=b) assign(u64 value){}
    @operator(a[b]) getElem(u64 index): u64 {return 0;}
}
Ввести полноэкранный режим Выйти из полноэкранного режима

Наконец, перепишем вручную пример

var A a;
a.getElem(0).assign(1);
Ввести полноэкранный режим Выйти из полноэкранного режима

Это не работает, правильное this теряется, и если по ошибке getElem() вернет A, то это будет r-значение, то есть не подходящее для присваивания.

Таким образом, в почти идеальном мире перегрузки оператора styx есть случаи, требующие специальной обработки.

Эта обработка заключается в просмотре подвыражений, вложенных в выражение, для распознавания и определения «макрооператора», который заменяет стандартный оператор. Рабочая версия A наконец-то готова.

struct A
{
    @operator(a[b]=c) assignToElem(u64 index; u64 value);
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Частный случай BoolExpression

Styx позволяет перегружать BoolExpression (например, true, false). Эта поддержка была добавлена, потому что styx позволяет пользователю писать сокращенные условия, такие как

var s8[] array;
if array do {} // lowered to `if array.length != 0 do ...`
var s8* ptr;
if ptr do {} // lowered to `if ptr != null do ...`
Войти в полноэкранный режим Выйти из полноэкранного режима

Перегруженные BoolExpressions используются, когда проверяются сокращенные условия. Для агрегата это имеет следующий вид

struct S
{
    @operator(true) function toCondition(): bool {return true;}
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Но может быть записано

struct S
{
    @operator(false) function toCondition(): bool {return true;}
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Но если вы понимаете, что это сбивает с толку, вы можете, например, использовать агрегатные экземпляры в качестве условия в условном выражении, например

struct S
{
    @operator(false) function toCondition(): bool {return true;}
}
var S s;
// same as `s.toCondition() ? 0 else 1`
var auto v = s ? 0 else 1; 
assert(s == 0);
Войти в полноэкранный режим Выйти из полноэкранного режима

Наконец

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

См. также

  • Styx @оператор
  • Перегрузка операторов в D.
  • Перегрузка операторов C++.

Оцените статью
devanswers.ru
Добавить комментарий