Превосходит ли препроцессор C99 Turing?

После обнаружения возможностей препроцессора Boost мне стало интересно: завершен ли препроцессор C99 Turing?

Если нет, то чего ему не хватает?

Ответ 1

Здесь приведен пример злоупотребления препроцессором для реализации машины Тьюринга. Обратите внимание, что для подачи вывода препроцессора обратно на его вход требуется внешняя сборка script, поэтому препроцессор сам по себе не является завершением Turing. Тем не менее, это интересный проект.

Из описания вышеупомянутого проекта:

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

Ответ Paul Fultz II впечатляет и, безусловно, ближе, чем я думал, что препроцессор может когда-либо получить, но это не настоящая машина Тьюринга. Препроцессор C имеет определенные ограничения, которые мешают ему выполнять произвольную программу, такую ​​как машина Тьюринга, даже если у вас есть бесконечная память и время. Раздел 5.2.4.1 C spec дает следующие минимальные пределы для компилятора C:

  • 63 уровней вложенности выражений в скобках в полном выражении
  • 63 важных начальных символа во внутреннем идентификаторе или имени макроса
  • 4095 макроидентификаторов, определенных одновременно в одном блоке перевода препроцессора
  • 4095 символов в логической строке источника

Для нижеприведенного механизма счетчика требуется определение макроса для каждого значения, поэтому ограничение определения макроса будет ограничивать количество циклов, которые вы можете зацикливать (EVAL(REPEAT(4100, M, ~)) приведет к поведению undefined). Это существенно ограничивает сложность программы, которую вы можете выполнить. Вложенность и сложность многоуровневых расширений могут также пострадать от одного из других ограничений.

Это принципиально отличается от ограничения "бесконечной памяти". В этом случае спецификация специально говорит о том, что совместимый со стандартами компилятор C требуется только для соответствия этим ограничениям, даже если он имеет бесконечное время, память и т.д. Любой входной файл, превышающий эти пределы, может обрабатываться в непредсказуемом или undefined способ (или откровенно отклоненный). Некоторые реализации могут иметь более высокие пределы или вообще не ограничивать, но считаются "специфичными для реализации", а не частью стандарта. Возможно, можно использовать метод Paul Fultz II для реализации чего-то вроде машины Тьюринга в какой-то конкретной реализации компилятора, которая не имеет конечных пределов, но в общем смысле "это можно сделать на любом произвольном, совместимом со стандартами предварительном процессоре C99", ответ - нет. Поскольку предел здесь встроен в сам язык, а не просто побочный эффект нашей неспособности построить бесконечный компьютер, я говорю, что нарушает полноту Тьюринга.

Ответ 2

Макросы Well напрямую не рекурсивно расширяются, но есть способы, которыми мы можем обойти это.

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

#define EMPTY()
#define DEFER(id) id EMPTY()
#define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)()
#define EXPAND(...) __VA_ARGS__

#define A() 123
A() // Expands to 123
DEFER(A)() // Expands to A () because it requires one more scan to fully expand
EXPAND(DEFER(A)()) // Expands to 123, because the EXPAND macro forces another scan

Почему это важно? Хорошо, когда макрос сканируется и расширяется, он создает контекст отключения. Этот контекст отключения приведет к тому, что токен, относящийся к текущему расширяющемуся макросу, будет окрашен в синий цвет. Таким образом, после окрашивания синего цвета макрос больше не будет расширяться. Вот почему макросы не рекурсивно расширяются. Тем не менее, отключенный контекст существует только во время одного сканирования, поэтому, откладывая расширение, мы можем предотвратить окрашивание наших макросов в синий цвет. Нам просто нужно применить к выражению больше сканирований. Мы можем сделать это с помощью этого макроса EVAL:

#define EVAL(...)  EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL2(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))
#define EVAL3(...) EVAL4(EVAL4(EVAL4(__VA_ARGS__)))
#define EVAL4(...) EVAL5(EVAL5(EVAL5(__VA_ARGS__)))
#define EVAL5(...) __VA_ARGS__

Теперь, если мы хотим реализовать макрос REPEAT с использованием рекурсии, сначала нам нужны некоторые операторы increment and decment для обработки состояния:

#define CAT(a, ...) PRIMITIVE_CAT(a, __VA_ARGS__)
#define PRIMITIVE_CAT(a, ...) a ## __VA_ARGS__

#define INC(x) PRIMITIVE_CAT(INC_, x)
#define INC_0 1
#define INC_1 2
#define INC_2 3
#define INC_3 4
#define INC_4 5
#define INC_5 6
#define INC_6 7
#define INC_7 8
#define INC_8 9
#define INC_9 9

#define DEC(x) PRIMITIVE_CAT(DEC_, x)
#define DEC_0 0
#define DEC_1 0
#define DEC_2 1
#define DEC_3 2
#define DEC_4 3
#define DEC_5 4
#define DEC_6 5
#define DEC_7 6
#define DEC_8 7
#define DEC_9 8

Далее нам нужно еще несколько макросов для логики:

#define CHECK_N(x, n, ...) n
#define CHECK(...) CHECK_N(__VA_ARGS__, 0,)

#define NOT(x) CHECK(PRIMITIVE_CAT(NOT_, x))
#define NOT_0 ~, 1,

#define COMPL(b) PRIMITIVE_CAT(COMPL_, b)
#define COMPL_0 1
#define COMPL_1 0

#define BOOL(x) COMPL(NOT(x))

#define IIF(c) PRIMITIVE_CAT(IIF_, c)
#define IIF_0(t, ...) __VA_ARGS__
#define IIF_1(t, ...) t

#define IF(c) IIF(BOOL(c))

#define EAT(...)
#define EXPAND(...) __VA_ARGS__
#define WHEN(c) IF(c)(EXPAND, EAT)

Теперь со всеми этими макросами можно написать рекурсивный макрос REPEAT. Мы используем макрос REPEAT_INDIRECT для рекурсивного возврата к себе. Это предотвращает окрашивание макроса в синий цвет, поскольку оно будет расширяться при другом сканировании (и использовании другого контекста отключения). Здесь мы используем OBSTRUCT, который будет откладывать расширение дважды. Это необходимо, потому что условный WHEN уже применяет одно сканирование.

#define REPEAT(count, macro, ...) \
    WHEN(count) \
    ( \
        OBSTRUCT(REPEAT_INDIRECT) () \
        ( \
            DEC(count), macro, __VA_ARGS__ \
        ) \
        OBSTRUCT(macro) \
        ( \
            DEC(count), __VA_ARGS__ \
        ) \
    )
#define REPEAT_INDIRECT() REPEAT

//An example of using this macro
#define M(i, _) i
EVAL(REPEAT(8, M, ~)) // 0 1 2 3 4 5 6 7

Теперь этот пример ограничен 10 повторами из-за ограничений счетчика. Точно так же, как повторный счетчик в компьютере будет ограничен ограниченной памятью. Несколько счетчиков повторов можно объединить вместе, чтобы обойти это ограничение, как на компьютере. Кроме того, мы могли бы определить макрос FOREVER:

#define FOREVER() \
    ? \
    DEFER(FOREVER_INDIRECT) () ()
#define FOREVER_INDIRECT() FOREVER
// Outputs question marks forever
EVAL(FOREVER())

Это попытается вывести ? навсегда, но в конечном итоге остановится, потому что больше не выполняется сканирование. Теперь вопрос в том, что если бы мы дали ему бесконечное количество сканирований, завершился бы этот алгоритм? Это известно как проблема остановки, и полнота Тьюринга необходима для доказательства неразрешимости проблемы остановки. Итак, как вы можете видеть, препроцессор может действовать как полный язык Тьюринга, но вместо того, чтобы ограничиваться конечной памятью компьютера, он ограничен ограниченным количеством примененных сканирований.

Ответ 3

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

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

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

Препроцессор C использует алгоритм Дейва Проссера (написанный Дейвом Проссером для команды WG14 в 1984 году). В этом алгоритме макрос окрашен синим цветом в момент первого раскрытия; рекурсивный вызов не продолжает расширять его, поскольку он уже окрашен синим в тот момент, когда начинается первое расширение. Таким образом, с конечным числом строк предварительной обработки невозможно совершать бесконечные вызовы функций (макросов), что характеризует мюрекурсивные операторы.

Препроцессор C может вычислять только сигма-рекурсивные операторы.

Подробности см. В курсе вычислений Марвина Л. Мински (1967) - Вычисления: конечные и бесконечные машины, Prentice-Hall, Inc., Энглвудские скалы, Нью-Джерси и т.д.

Ответ 4

It Turing завершается в пределах (как и все компьютеры, так как у них нет бесконечной ОЗУ). Ознакомьтесь с тем, что вы можете сделать с помощью Boost Preprocessor.

Изменить в ответ на вопрос:

Основное ограничение на Boost - это максимальная глубина расширения макроса, которая является специфичной для компилятора. Кроме того, макросы, реализующие рекурсию (FOR..., ENUM... и т.д.), Не являются действительно рекурсивными, они просто появляются таким образом благодаря кучке почти идентичных макросов. В большой картине это ограничение не отличается от максимального размера стека на фактически рекурсивном языке.

Единственные две вещи, которые действительно необходимы для ограниченной Тьюринга-полноты (совместимость Тьюринга?), - это итерация/рекурсия (эквивалентные конструкции) и условное ветвление.