Как вы пишете рекурсивную функцию, используя нерекурсивный стек?

Чтобы попытаться реализовать PEG в JavaScript, который не приводит к сбою старых браузеров из, я хотел бы сделать грамматику выражения синтаксического анализа, которая анализирует строку нерекурсивным образом. Как ты делаешь это? Он чувствует изгиб ума.

Скажем, у вас есть такая структура:

  • A grammar имеет много выражений
  • An expression имеет много matchers
  • A matcher имеет много tokens (или что-то другое для него лучше)
  • A token может либо указывать на другой expression, либо быть примитивной строкой или регулярным выражением. Поэтому, если это указывает на другое выражение, здесь начинается рекурсия.

Итак, скажите, что вы определяете иерархию следующим образом:

var grammar = new Grammar('math');
var expression = grammar.expression;

expression('math')
  .match(':number', ':operator', ':number', function(left, operator, right){
    switch (operator) {
      case '+': return left + right;
      case '-': return left - right;
      case '*': return left * right;
      case '/': return left / right;
    }
  });

expression('number')
  .match(/\d+/, parseInt);

expression('operator')
  .match('+')
  .match('-')
  .match('*')
  .match('/');

var val = grammar.parse('6*8'); // 42

Когда вы вызываете grammar.parse, он начинается с корневого выражения (которое имеет то же имя, что и "математика" ). Затем он выполняет итерацию через каждый соединитель, затем каждый токен, и если токен является выражением, он рекурсирует. В основном это (парсер будет отслеживать смещение/позицию строки, которая соответствует шаблонам, это просто псевдокод):

function parse(str, expression, position) {
  var result = [];

  expression.matchers.forEach(function(matcher){
    matcher.tokens.forEach(function(token){
      var val;
      if (token.expression) {
        val = parse(str, token.expression, position);
      } else {
        val = token.parse(str, position);
      }
      if (val) result.push(val);
    });
  });

  return result;
}

parse('6*8', grammar.root, 0);

Итак, для простого выражения типа 6*8 очень мало рекурсии, но вы можете быстро перейти к сложному выражению со множеством уровней вложенности. Плюс умножьте вложенность на все те вложенные для циклов, и стек становится большим (я фактически не использую forEach, я использую для циклов, но в цикле for он вызывает функцию большую часть времени, поэтому он заканчивается в то же время).

Вопрос в том, что как вы "сглаживаете это"? Вместо того, чтобы делать рекурсию, как вы это делаете, так это примерно так:

while (token = stack.pop()) {
  val = token.parse(val);
  if (val) result.push(val);
}

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

Ответ 1

В общем, что вы делаете, вы пишете стек в коде, и вы помещаете свои "локальные" переменные в объект контекста "стековый фрейм", который вы держите в этом стеке. Затем, когда у вас будет "рекурсивный вызов", вы сохраняете текущий стек стека и создаете новый для нового текущего контекста. Выполнение "возврата" - это всего лишь вопрос об отмене операции. Это не особенно сложно, но это делает код немного беспорядочным. Единственное, на что нужно обратить внимание, это то, что вы добираетесь до нижней части стека в тот момент, когда вы закончили синтаксический разбор выражения (так что трейлинг-токены и пропущенные токены не вызывают проблем).

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

Если у вас есть время, подумайте о написании (или использовании чужого) парсера LR (1). Они поддерживают очень небольшой системный стек и обрабатывают множество злых случаев в грамматике лучше, чем ваша домашняя грамматика LL (k). Тем не менее, они значительно более загадочны в том, как они работают, чем то, что у вас есть.

Ответ 2

Я больше всего ищу общий способ отслеживания рекурсивного состояния нерекурсивным образом.

Используйте pushing и popping в стеках (массивы).
И было бы проще, если бы у вас были goto's.
A (факториальный) подход в VBA (более понятный из-за goto's).

Option Explicit
Sub Main()
  MsgBox fac(1)
  MsgBox fac(5)
End Sub
Function fac(n&)
  Dim answer&, level&, stackn&(99)
  level = 0
zentry:
  If n = 1 Then answer = 1: GoTo zreturn
  level = level + 1 ' push n
  stackn(level) = n
  n = n - 1 ' call fac(n-1)
  GoTo zentry
zreturn:
  If level = 0 Then fac = answer: Exit Function
  n = stackn(level) ' pop n
  level = level - 1
  answer = n * answer ' factorial
  GoTo zreturn
End Function

Тот же подход в javascript.

console.log(fac(1));
console.log(fac(5));
function fac(n) { // non-recursive
  var answer, level; 
  var stackn = []; 
  level = 0;
  while (true) { // no goto's
    if (n == 1) { answer = 1; break; }
    level = level + 1; // push n
    stackn[level] = n;
    n = n - 1; } // call fac(n-1) 
  while (true) { // no goto's
    if (level == 0) { return answer; }
    n = stackn[level]; // pop n
    level = level - 1;
    answer = n * answer; } // factorial
  }