Skip to content

Специализация без шаблона #251

@Mazdaywik

Description

@Mazdaywik

UPD 24.10.2020: название заявки обновлено, т.к. именно это требуется в комментариях.

Мотивация

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

Например, в своём недавнем письме в рассылку refal@botik.ru я разбирал пример с интерпретатором диалекта Форта, где пришлось написать так:

$SPEC Loop e.stack (e.CODE) (e.DICT);

Loop {
  e.Stack (e.Code_) (e.Dict)
    , e.Code_
    : {
        /* empty */ = e.Stack;

        DUP e.Code
          = e.Stack : e.Stack^ t.Top
          = <Loop e.Stack t.Top t.Top (e.Code) (e.Dict)>;

        DROP e.Code
          = e.Stack : e.Stack^ t.Top
          = <Loop e.Stack (e.Code) (e.Dict)>;
        …
      }
}

Чтобы функцию можно было специализировать по e.CODE потребовалось написать одно предложение с блоком. Более естественный вариант выглядел бы так:

$SPEC Loop e.stack (e.CODE) (e.DICT);

Loop {
  e.Stack (/* empty */) (e.Dict) = e.Stack;

  e.Stack (DUP e.Code) (e.Dict)
    = e.Stack : e.Stack^ t.Top
    = <Loop e.Stack t.Top t.Top (e.Code) (e.Dict)>;

  e.Stack (DROP e.Code) (e.Dict)
    = e.Stack : e.Stack^ t.Top
    = <Loop e.Stack (e.Code) (e.Dict)>;

  …
}

Но нельзя, e.CODE здесь уточняется не в e-переменную, а в выражение.

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

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

Почему так не было сделано? Подход с возможностями прогонки требует наличия функции обобщённого сопоставления с образцом, которая может решить эту систему уравнений. Но на момент начала работ по обоим оптимизациям (прогонка #122, специализация #126) этой функциональности не было, был её ограниченный вариант, допускающий лишь успешные сопоставления. Прогонку делал Кирилл Ситников @InfiniteDisorder, специализацию — Дарья Сухомлинова @StrixSeloputo. Поскольку функция решения уравнений есть содержательная часть работы Кирилла, было решено ограничить Дарью базовым вариантом специализации.

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

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

Если статические параметры сужаются в L-выражения (которые в совокупности не содержат повторных t- и e-переменных), то специализация с элементами прогонки всегда будет успешной. Иначе возможен вариант Undefined, что в этом случае делать — не очевидно.

  • Базовый вариант — отказываться от специализации этого вызова.
  • Тонкий вариант — специализация только предшествующих предложений. Для предложений, начиная с первого Undefined, генерируется как при прогонке остаточная функция Func*N, которая вызывается в неуспешном предложении. Побочный эффект: оптимизация будет увеличивать число шагов.
  • Другой, ещё более тонкий вариант — динамически обобщать аргумент. Поговорим о нём подробнее.

Что значит: сопоставление не удалось разрешить? Это значит, что при решении системы получилось уравнение вида E : Le, где E — фрагмент аргумента, а Le — фрагмент образца и с этим уравнением алгоритм решения справиться не может. (Частный случай E1 : E2, возникающий при наличии двух присваиваний E1 ← var и E2 ← var можно трактовать как уравнение 〈E1, E2〉 : 〈var, var〉, не будем его подробно рассматривать.)

Уравнение вида var : Le решить в каком-то смысле можно при любом Le (при соответствии типа переменной, конечно) — его решением можно назвать сужение var → Le (но есть тонкости с переменными). Решение для системы (var1) … (varN) : (Le1) … (LeN) вида var1 → Le1, …, varN → LeN можно подставить в L-образец, если порядок переменных var1varN тот же, что и в системе уравнений. В том смысле, что при подстановке не должен меняться порядок следования открытых e-переменных (см. #249).

Если решатель нашёл неразрешённое уравнение E : Le (или несколько таких уравнений), то в исходном аргументе можно заменить E на новую переменную и заново выполнить прогонку. Т.е., в каком-то смысле мы часть аргумента делаем динамической.

Базовый вариант должен быть реализован в первую очередь. Любая пометка оптимизации ($DRIVE, $INLINE, $SPEC) это всего лишь совет компилятору оптимизировать функцию. Если оптимизация возможна, то она выполняется. Если нет, остаётся как есть. Такое решение в духе Си++, где ключевые слова inline и register тоже являются исключительно советами. Если программист сам написал предложение, где статические параметры в совокупности отображаются в не-L-кортеж образцов, значит он готов к тому, что функция иногда специализироваться не сможет.

Программа максимум: специализация без шаблона

Здесь предлагается специализируемые функции просто помечать ключевым словом $SPEC, например в форме $SPEC Map { … }.

Если реализована программа-минимум, то описание

$SPEC Func;

должно считаться эквивалентным

$SPEC Func e.ALL-STATIC-ARG;

Вообще, зачем нужен был шаблон в исходной постановке задачи? Изначально для выделения статических (шаблонных) параметров одновременно с желанием сохранить привычный синтаксис функции. Альтернативной могло быть использование особого синтаксиса для шаблонных параметров. Например, такого:

Map [t.Func] {
  t.Next e.Rest = <Apply t.Func t.Next> <Map [t.Func] e.Rest>;
  /* empty */ = /* empty */;
}

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

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

Например, в функции

$SPEC Replace (e.FROM) (e.TO) e.text;

Replace {
  (e.From) (e.To) e.Text-B e.From e.Text-E
    = e.Text-B e.To <Replace (e.From) (e.To) e.Text-E>;

  (e.From) (e.To) e.Text = e.Text;
}

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

$SPEC Replace {
  (e.From) (e.To) e.Text-B e.From e.Text-E
    = e.Text-B e.To <Replace (e.From) (e.To) e.Text-E>;,

  (e.From) (e.To) e.Text = e.Text;
}

Test { (e.X) (e.Y) (e.Z) = <Replace ('Foo') ('Bar') e.X e.Y e.Z> }

возникнет нетривиальное уравнение e.X e.Y e.Z : e.Text-B 'Foo' e.Text-E, которое решить невозможно и придётся динамически обобщить e.X e.Y e.Z до одной переменной, скажем, e.0.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions