【问题标题】:Parsing loops in a javascript interpreter在 javascript 解释器中解析循环
【发布时间】:2021-09-18 00:31:08
【问题描述】:

我一直在探索用 javascript 编写一个非常基本/有限的解释器作为练习。在我介绍了 LOOP 的概念之前,一切都很顺利。

给定以下脚本:

LOOP 2
  A
  LOOP 3
    B
  END
  LOOP 4
    C
    LOOP 5
      D
    END
    E
  END
  F
END

算法应按以下顺序访问内部标记:

ABBBCDDDDDECDDDDDECDDDDDECDDDDDEFABBBCDDDDDECDDDDDECDDDDDECDDDDDEF

以下方法可以解决问题,但它需要对令牌进行大量迭代。这是对我之前使用的手动扩展循环的切片方法的改进,但远非最佳。

/**
 * In practice, we'll grab each token as we read the script,
 * but to keep this simple and focus on the loop algorithm,
 * we can cheat and make an array of all the tokens.
 */

const getTokens = (s) => s.replace(/[\W_]+/g, " ").split(" ").filter(Boolean);

/* Temp vars - ideally, I'd like to solve this with arrays. */
const start = []; // Loop start indices
const end = []; // Loop end indices
const counts = []; // Times to loop
const completed = []; // Loops completed

for (let i = 0; i < tokens.length; i++) {
  const token = tokens[i];

  if (token === "LOOP") {
    if (start.length == 0 || i > start[start.length - 1]) {
      // Add new loop index if we haven't seen it before
      start.push(i); // Store the loop index
      counts.push(Number(tokens[i + 1])); // The loop count is always next LOOP token
      completed.push(0); // Initialize with 0 completed at index

      // Find the end index for the loop
      // Note: This is the slowest part.
      let skip = 0;
      for (let j = i + 2; j < tokens.length; j++) {
        if (tokens[j] == "LOOP") {
          skip++; // Increase nest depth
        } else if (tokens[j] == "END") {
          if (skip == 0) {
            end.push(j); // Found matching loop close
            break;
          }
          skip--;
        }
      }
    }

    i++; // Skip over the loop count
    continue;
  } else if (token === "END") {
    let j;
    for (j = 0; j < end.length; j++) {
      if (end[j] == i) break; // Found matching end index
    }
    const isCompleted = completed[j] == counts[j] - 1;
    if (!isCompleted) {
      i = start[j] + 1;
      completed[j]++;
      for (let k = j + 1; k < start.length; k++) {
        completed[k] = 0; // Reset nested loops in between
      }
    }
    continue;
  }

  console.log(tokens[i]);
}

https://jsfiddle.net/5wpa8t4n/

有什么更好的方法来完成这种基于数组的方法,使用单遍脚本,或者最坏的 2 遍,而不是 N-LOOP 遍?

【问题讨论】:

  • "解析器应该返回" - 不应该。将解析器(创建 AST 或字节码)与解释器(创建输出)分开。
  • 我知道用字节码创建一个抽象语法树可能会有所帮助,但它不一定会提高我的循环解析算法的时间复杂度,这是我的主要目标。我已经在此处确定了这个问题的范围,以突出我想要在一个易于理解的 sn-p 中解决的确切问题。
  • 那我不明白你的目的。只有不执行代码的解析器才能高效并使用固定次数的传递。实际运行循环以产生您想要的输出的解释器总是必须重复通过循环,其时间复杂度不能比输出大小线性好。
  • 现在,我的方法是找到每个“循环”,并通过查看所有标记找到每个“循环”。这是在我真正循环遍历所有具有跳跃能力的令牌之前的初步设置工作。我一直在尝试想出一种方法来读取一次标记并执行跳转,但算法让我望而却步。目标实际上是完成上述脚本的工作,但减少对所有令牌的扫描。
  • @noobcode - 你的目标很明确 - Bergi 只希望你认识并使用正确的术语。

标签: javascript loops ecmascript-6 computer-science interpreter


【解决方案1】:

在开始解释循环时,您不需要知道匹配的循环end 的位置。你需要记录的是遇到下一个end时要跳回的位置,但在那之前继续逐个令牌解释。

这些位置,连同各自的计数器,可以存储在堆栈结构中。

const script = `
DO
  A
  DO
    B
  LOOP 3
  DO
    C
    DO
      D
    LOOP 5
    E
  LOOP 4
  F
LOOP 2
`

const parse = (script) =>
  script
    .replace(/[\W_]+/g, " ")
    .split(" ")
    .filter(Boolean);

const interpret = (code) => {
  let loops = []; // Active loops: iteration count and jump target
  let ip = 0; // instruction pointer

  let result = "";
  while (ip < code.length) {
    const instruction = code[ip];
    switch (instruction) {
      case "DO": {
        ++ip;
        loops.push({count: 0, start: ip});
      } break;
      case "LOOP": {
        const limit = Number(code[++ip]);
        const {count, start} = loops.pop();
        if (count < limit) {
          loops.push({count: count+1, start});
          ip = start; // jump back
        } else {
          ++ip;
        }
      } break;
      default: {
        ++ip;
        result += instruction; // a print statement
      } break;
    }
  }
  return result;
};

console.log(interpret(parse(script)));

我已经稍微简化了结构以使用do-while 循环,所以我永远不必跳过循环体。在解析器发出的真正字节码中,跳转目标(来回)将是指令本身的一部分,并且只有计数“变量”需要存储在堆栈中。跳转目标永远不会改变,因此您只需在 parse 函数中生成一次即可。

【讨论】:

  • 哦,这太棒了!我在堆栈结构上使用 pop() 陷入了困境,但这并不完全正确。我现在明白你所说的基于开关的解释器是什么意思了。
猜你喜欢
  • 2015-03-01
  • 1970-01-01
  • 2013-08-31
  • 1970-01-01
  • 1970-01-01
  • 2018-01-16
  • 2015-05-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多