Проверка проблемы остановки на самореализуемой псевдосборке - PullRequest
0 голосов
/ 09 марта 2020

Я написал очень простую реализацию того, что может быть похоже на сборку / машинный код.

Он даже способен к рекурсии, как в этом примере:

9 6
IFEQ R0,0
RET 1
ENDIF
MOV R1,R0
SUB R1,1
CALL R1
MOV R2,R9
MUL R2,R0
RET R2

Вывод: 720 (факториал 6)

Описание:

9 = Program Lines
6 = Program Input. Will be set to registry R0 value at class construction
CALL = calls the program again with the passed value (recursion)
RET = returns the program with the specified value. Sets registry R9 value to output value.

R0 to R9 -> general purpose registry.
R0 - program input value
R9 - program output value

-edit: Программные команды: MOV, ADD, SUB, MUL, DIV, MOD, IFEQ, IFNEQ, IFG, IFGE, IFL, IFLE, ENDIF, CALL, RET

Однако программа может войти в бесконечный цикл / рекурсию. Например:

2 0
CALL 10
RET 1 //will never be reached

Как проверить, войдет ли MY программа в бесконечный цикл / рекурсию?

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

#include <iostream>
#include <map>
#include <string> //std::getline
#include <sstream>
#include <vector>

namespace util
{
    template<typename I>I readcin(I& input) {
        std::cin >> input;
        std::cin.clear(); std::cin.ignore();
        return input;
    }
    template<typename I, typename...O> I readcin(I& input, O&... others) {
        readcin(input);
        return readcin(others...);
    }
}

//operations
enum OP
{
    MOV, ADD, SUB, MUL, DIV, MOD,
    IFG, IFL,
    IFEQ, IFGE, IFLE,
    IFNEQ,
    CALL,
    RET,
    ENDIF,
};
std::map<std::string, OP> OPTABLE
{
    {"MOV", MOV}, {"ADD", ADD}, {"SUB", SUB}, {"MUL", MUL}, {"DIV", DIV}, {"MOD", MOD},
    {"RET", RET},
    {"IFG", IFG}, {"IFL", IFL},
    {"IFNEQ", IFNEQ}, {"IFEQ", IFEQ}, {"IFGE", IFGE}, {"IFLE", IFLE},
    {"CALL", CALL},
    {"ENDIF", ENDIF}
};
//registry index
enum RI
{
    R0, R1, R2, R3, R4, R5, R6, R7, R8, R9, RI_MAX
};
std::map<std::string, RI> RITABLE =
{
    {"R0", R0}, {"R1", R1}, {"R2", R2}, {"R3", R3}, {"R4", R4}, {"R5", R5},
    {"R6", R6}, {"R7", R7}, {"R8", R8}, {"R9", R9}
};

struct Instruction
{
    OP op;
    RI r1;
    int r2value;
    Instruction() = delete;
    Instruction(OP operation, RI firstRegister, int _2ndRegValue = -1)
    {
        op = operation;
        r1 = firstRegister;
        r2value = _2ndRegValue;
    }
};


class Assembly
{

private:
    int REG[RI::RI_MAX] {0};
    int GetRegistryValue(RI ri) const { return REG[ri]; }
    void SetRegistryValue(RI ri, int val) { REG[ri] = val; }

    enum CMP_FLAG{ CMP_FAIL, CMP_OK };
    CMP_FLAG flag { CMP_OK };
    CMP_FLAG GetFlag() const { return flag; }
    void SetFlag(bool setFlag) { flag = static_cast<CMP_FLAG>(setFlag); }

    std::vector<std::string> programLines;

    OP ExtractOP(const std::string& line);
    RI ExtractRI(const std::string& line, OP op);
    int Extract2ndRIval(const std::string& line, OP op);
public:
    void addCommand(const std::string& line) { programLines.push_back(line); }
    void Execute();

    Assembly() = delete;
    Assembly(int inputValue) { REG[R0] = inputValue; }
    int ReturnValue() const { return REG[R9]; }
private:
    //recursion only
    Assembly(int inputValue, const std::vector<std::string>& progLines) {
        REG[R0] = inputValue;
        programLines = progLines;
        this->Execute();
    }
};

OP Assembly::ExtractOP(const std::string& line)
{
    std::istringstream issline{ line };
    std::string operation;
    issline >> operation;

    return OPTABLE[operation];
}

RI Assembly::ExtractRI(const std::string& line, OP op)
{
    auto space = line.find(' ');
    if(op <= IFNEQ){
        auto comma = line.find(',');
        return RITABLE[std::string(line.begin() + space + 1, line.begin() + comma)];
    }
    return RI_MAX;
}

int Assembly::Extract2ndRIval(const std::string& line, OP op)
{
    if(op == ENDIF) {
        return -1;
    }

    std::size_t spaceOrComma;
    if(op == CALL || op == RET) {
        spaceOrComma = line.find(' ');
    } else {
        spaceOrComma = line.find(',');
    }

    std::string opval = std::string(line.begin() + spaceOrComma + 1, line.end());
    auto it = RITABLE.find(opval);
    if(it != RITABLE.end()){
        return this->REG[it->second];
    }
    auto num = std::atoi(opval.c_str());
    return num;
}

void Assembly::Execute()
{
    for(const std::string& line : programLines)
    {
        OP op = ExtractOP(line);
        RI r1 = ExtractRI(line, op);
        int r2value = Extract2ndRIval(line, op);

        Instruction command ( op, r1, r2value );

        if(GetFlag() == CMP_FAIL)
        {
            if(command.op == ENDIF){
                SetFlag(CMP_OK);
            }
            continue;
        }

        switch(command.op)
        {
            case MOV: { SetRegistryValue(command.r1, command.r2value); } break;
            case ADD: { SetRegistryValue(command.r1, REG[command.r1] + command.r2value); } break;
            case SUB: { SetRegistryValue(command.r1, REG[command.r1] - command.r2value); } break;
            case MUL: { SetRegistryValue(command.r1, REG[command.r1] * command.r2value); } break;
            case DIV: { SetRegistryValue(command.r1, REG[command.r1] / command.r2value); } break;
            case MOD: { SetRegistryValue(command.r1, REG[command.r1] % command.r2value); } break;

            case IFEQ:  { SetFlag(GetRegistryValue(command.r1) == command.r2value); } break;
            case IFNEQ: { SetFlag(GetRegistryValue(command.r1) != command.r2value); } break;
            case IFG:   { SetFlag(GetRegistryValue(command.r1) > command.r2value); } break;
            case IFL:   { SetFlag(GetRegistryValue(command.r1) < command.r2value); } break;
            case IFGE:  { SetFlag(GetRegistryValue(command.r1) >= command.r2value); } break;
            case IFLE:  { SetFlag(GetRegistryValue(command.r1) <= command.r2value); } break;

            case RET:
            {
                SetRegistryValue(R9, command.r2value);
                return;
            }break;

            //oh boy!
            case CALL:
            {
               // std::cout << "value to call:" << command.r2value << std::endl;
                Assembly recursion(command.r2value, this->programLines);
                SetRegistryValue(R9, recursion.ReturnValue());
            }break;
        }
    }
}

int main()
{
    while(true)
    {
        int pl, input;
        util::readcin(pl, input);
        if(pl == 0){
            break;
        }

        Assembly Asm(input);
        for(auto i=0; i<pl; ++i)
        {
            std::string line;
            std::getline(std::cin, line);
            Asm.addCommand(line);
        }
        Asm.Execute();

        std::cout << Asm.ReturnValue() << std::endl;
    }
    return 0;
}

Ответы [ 4 ]

3 голосов
/ 12 марта 2020

Единственный способ проверить, застряла ли программа в бесконечном l oop в общем случае, это проверить, что программа вошла в то же состояние, что и предыдущее состояние. Если он вошел в то же самое состояние ранее, то он должен продолжить выполнение в al oop, возвращаясь к тому же состоянию снова и снова, следуя той же последовательности шагов. В реальных программах это по существу невозможно из-за огромного числа возможных состояний, в которых может находиться программа, но ваш язык ассемблера допускает только гораздо более ограниченное количество возможных состояний.

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

В других словами, единственное состояние, которое необходимо проверить, - это значение R0 в начале программы. Так как это значение хранится в int, оно может иметь только 2 ^ 32 возможных значений в любой распространенной реализации C ++, поэтому разумно и легко проверить принудительно, посмотреть, застревает ли данная программа с данным вводом в бесконечном l oop.

На самом деле, можно использовать запоминание возвращаемых значений для грубой силы, чтобы проверить все возможные входные данные за время O (N) и пространство O (N), где N - число возможных входы. Есть несколько способов сделать это, но я бы создал три вектора с количеством элементов, равным количеству возможных входных данных. Первый вектор представляет собой bool (бит) вектор, который записывает, был ли запомнен данный ввод, второй вектор также является bool вектором, и он записывает, использовался ли уже данный ввод в стеке вызовов, второй вектор - это вектор int, который записывает результат и использует связанный список входных значений для создания стека вызовов для экономии места. (В приведенном ниже коде эти векторы называются is_memoized, input_pending и memoized_value соответственно.)

Я бы взял ваш интерпретатор l oop и переписал бы его как нерекурсивный, что-то как этот псевдокод:

input = reg[R0]
if is_memoized[input]: 
    reg[R9] = memoized_value[input]
    return
input_pending[input] = true
memoized_value[input] = input  // mark the top of the stack

while true:
    for command in program:

        ...

        if command.op == CALL:
             argument = command.r2value

             if input_pending[argument]: 
                 // Since this input value is ready been used as input value 
                 // somewhere on the call stack this the program is about to enter
                 // an identical state as a previous state and so is stuck in
                 // a infinite loop.
                 return false  // program doesn't halt

             if is_memoized[argument]:
                 REG[R9] = memoized_value[argument]
             else:
                 memoized_value[argument] = input   // stack the input value

                 input = argument
                 REG = [input, 0, 0, 0, 0, 0, 0, 0, 0, 0]
                 input_pending[input] = true
                 break  // reevaluate the program from the beginning.

        if command.op == RET:
              argument = command.r2value
              stacked_input = memoized_value[input]
              input_pending[input] = false
              if stacked_input == input:  // at the top of the stack
                  REG[R9] = argument
                  return true   // program halts for this input

              is_memoized[input] = true
              memoized_value[input] = argument
              input = stacked_input
              REG = [input, 0, 0, 0, 0, 0, 0, 0, 0, 0] 
              break  // reevaluate the program from the beginning

Затем вы будете называть этот интерпретатор l oop для каждого возможного ввода, что-то вроде этого:

for i in all_possible_inputs:
     if not program.execute(input = i):  // the function written above
          print "program doesn't halt for all inputs"
          return
print "program halts for all inputs"

Рекурсивная версия должна быть быстрее поскольку ему не нужно переоценивать программу для каждой немеизированной инструкции CALL в программе, но в худшем случае для этого потребуются сотни гигабайт стекового пространства. Эта нерекурсивная версия требует только 17 ГБ памяти. В любом случае это все еще O (N) пространство и время, вы просто уменьшаете один постоянный фактор, а другой - больше.

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

1 голос
/ 12 марта 2020

Я так понимаю, вы ищете нестандартное мышление.

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

Даже моя архитектура процессов совместной работы не выполняет sh этого. Это просто запрещает им из-за беспорядка, который они делают. Это все еще составлено из языка, который содержит их. Например, вы можете использовать IF для прерывания, возврата или продолжения, но не для других операций. Вызовы функций недопустимы. Я создал такие ограничения для достижения управляемости. Однако даже совместная организация не удаляет управляющие структуры из языка, чтобы предотвратить их неправильное использование.

Моя архитектура подключена к сети через мой профиль с факториальным примером в моей статье W.

1 голос
/ 11 марта 2020

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

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

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

Мне кажется, что единственная интересная инструкция по переходу - call. Подобные IF всегда будут создавать разные конфигурации, потому что они недостаточно выразительны для создания итерации (отскок назад и повтор).
Как call меняет конфигурацию? Он устанавливает номер строки в 1, а все регистры (кроме r0) в ноль.
Таким образом, условие для вызова для создания одинаковой конфигурации сводится к тому, чтобы иметь одинаковый вход.

Если вы проверите, в реализации call, было ли значение операнда уже использовалось ранее (в симуляции), вы можете сказать, что программа будет l oop навсегда. Если регистр имеет размер n , то возможными состояниями являются O (2 n ), что, как правило, много.
Вы должны быть готовы отказаться после (возможного настраиваемый) порог. Или в вашем случае, когда ваши регистры int, большинство реализаций C ++ имеют 32-битную int, и современные машины могут обрабатывать 512 МБ битовой карты размером 2 ^ 32 бит. (std::vector<bool> реализует упакованное растровое изображение, например; индексировать его с unsigned, чтобы избежать отрицательных значений). Таблица ha sh является еще одной альтернативой (std::unordered_set<int>). Но если бы вы использовали более широкий тип для своих регистров, состояние было бы слишком большим, чтобы практически записать каждый возможный, и вам потребовалось бы некоторое ограничение. Ограничение является своего рода встроенным в вашу реализацию, так как вы переполните стек вызовов C ++ (глубина рекурсии C ++), прежде чем увидите где-нибудь около 2 ^ 32 повторов моделируемой машины.

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

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


Чтобы проверить завершение программы на каждом входе, вы должны продолжить недетерминированно и символически .
Есть две проблемы: ветви и входное значение.

Это известная теорема о том, что NDTM может быть моделируется ТМ за экспоненциальное число шагов по сравнению с шагами NDTM.
Единственными проблемными c инструкциями являются IF, поскольку они создают недетерминизм.
Вы можете использовать несколько подходов:

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

Все они эквивалентны.

Входное значение неизвестно, поэтому трудно сказать, окажется ли call в той же конфигурации. Возможный подход - записать все изменения во входной регистр, например, вы можете получить описание типа «sub (add (add (R0, 1), 4), 5);».

Это описание должно быть легко манипулировать, так как легко увидеть, что в приведенном выше примере R0 не изменилось, потому что вы получаете "sub (add (R0, 5), 5);" а затем "R0;".
Это работает, полагаясь на арифметику l aws, вы должны позаботиться об обратных операциях, элементах тождества (1 * a = a) и переполнении.
В основном вам нужно для упрощения выражения.
Затем можно проверить, изменился ли вход в заданный момент в смоделированном времени.

0 голосов
/ 09 марта 2020

Как проверить, войдет ли программа в бесконечный цикл / рекурсию?

На практике; решить проблему остановки тривиально. Теоретически это невозможно.

Причина, по которой люди думают, что проблему остановки невозможно решить, состоит в том, что вопрос построен как ложная дилемма (https://en.wikipedia.org/wiki/False_dilemma). В частности, вопрос просит определить, будет ли программа всегда останавливаться или никогда не остановится; но есть и третья возможность - иногда останавливаться (а иногда и не останавливаться). В качестве примера, если это так, представьте себе программу, которая спрашивает пользователя, хотят ли они навсегда остановиться или выйти немедленно (и правильно делает то, что запросил пользователь). Обратите внимание, что все нормальные приложения похожи на это - они не должны выходить до тех пор, пока пользователь не скажет им.

Более правильно; на практике существует 4 варианта:

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

Конечно, с этими 4 возможностями вы можете сказать, что вы ' Мы создали «средство решения проблем», просто классифицируя каждую программу как «неопределенную», но это не будет хорошим решением. Это дает нам своего рода систему оценки - чрезвычайно хорошие «решатели проблемы остановки» редко классифицируют программы как «неопределенные», а чрезвычайно плохие «решатели проблемы остановки» часто классифицируют программы как «неопределенные».

Итак; Как вы создаете хороший "решатель проблем"? Это включает 2 части - генерацию потоковых графиков управления (https://en.wikipedia.org/wiki/Control-flow_graph) для каждой функции и графика вызовов (https://en.wikipedia.org/wiki/Call_graph) для всей программы; и отслеживание значений.

Графики потока управления и График вызовов

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

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

Анализируя полученные графики, вы можете классифицировать тривиальные программы как" запускаемые до тех пор, пока что-то внешнее не заставляет их останавливаться "или" всегда останавливающие себя "(например, этого достаточно для классификации исходного кода OP). as "работает до тех пор, пока что-то внешнее не заставит его остановиться"); но большинство программ все еще будут "неопределенными".

Отслеживание значений

Отслеживание значений (пытается) отслеживать возможные значения, которые могут быть в любом регистр / переменная / ячейка памяти в любой момент времени. Например, если программа считывает 2 байта без знака с диска в 2 отдельные переменные, вы знаете, что обе переменные будут иметь значение от 0 до 255. Если эти переменные умножены, вы знаете, что результатом будет значение от 0 * 0 до 255 * 255. ; если эти переменные добавлены, вы знаете, что результатом будет значение от 0 + 0 до 255 + 255; и др c. Конечно, тип переменной дает абсолютные максимально возможные диапазоны, даже для сборки (где нет типов) - например, (не зная, подписан он или нет), вы знаете, что 32-битное чтение из памяти вернет значение от -2147483648 до 4294967296 .

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

Вот где все становится сложнее - повышение качества «практического решения проблем остановки» требует увеличения сложности / сложности отслеживания значений. Я не знаю, возможно ли написать идеальный «решатель проблемы остановки» (который никогда не возвращает «неопределенный»), но я знаю, что можно написать «решатель проблемы остановки», который достаточен почти для всех практических целей (что возвращает «неопределенный» так редко, что это никого не волнует).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...