javascript - 如何在 JavaScript 中实现具有文字状态和转换的基本迭代下推自动机解析算法?
问题描述
我们希望创建一个下推自动机(PDA),它使用以下“字母表”(字母表是指一组唯一的符号字符串/键):
+aaa
+bbb
+ccc
+ddd
+eee
-aaa
-bbb
-ccc
-ddd
-eee
此字母表中的“符号”(带有 + 或 - 前缀的 3 个字母序列)用于制作树。以下是一些示例树:
+eee-eee
+aaa+bbb-bbb-aaa
+bbb+aaa+ccc+eee-eee-ccc+ccc-ccc+ddd+ccc+eee-eee-ccc-ddd-aaa-bbb
可视化为一棵实际的树,它们更像是:
+eee
-eee
+aaa
+bbb
-bbb
-aaa
+bbb
+aaa
+ccc
+eee
-eee
-ccc
+ccc
-ccc
+ddd
+ccc
+eee
-eee
-ccc
-ddd
-aaa
-bbb
所以给定这个字母表和这些示例树,问题是你将如何编写一个通用的下推自动机来解析这些树。规则是:
- 任何字母对(打开/关闭对)都可以有任意数量的嵌套子级,并且将哪些字母对嵌套为子级并不重要。
您将如何在 JavaScript 中编写下推自动机来将字符串解析为 AST?
我的意思是,实现必须从字面上具有stack
,states
和transitions
. 我的意思是,不实现临时解析器,甚至不实现递归下降解析器。这应该是一个迭代的 while 循环,以某种方式循环转换,而不是使用递归。
输出应该是一个非常简单的“AST”,看起来像这样(对于+aaa+bbb-bbb-aaa
树):
{
"type": "aaa",
"children": [
{
"type": "bbb",
"children": []
}
]
}
我想知道如何构建一个 PDA,以便在我使用自定义编程语言的特定情况下,我可以转换我正在工作的相当复杂和复杂的 AST,并将其解析为对象图。这个问题在 SO 上写得太复杂,也太难简化,这就是为什么我在这里询问 JavaScript 中非常基本的 PDA 实现的原因。
我有兴趣了解在执行 PDA 算法时如何跟踪每个分支点的上下文以及转换的样子。
注意:我听说过/见过偶尔提到的“2-state”PDA。听起来您有一种状态来进行计算,还有一种“最终”状态。甚至可能只是一个状态。如果有可能做到这一点,那就太酷了。如果没有,请不要担心。
如有必要,您可以先编写一个上下文无关语法并使用它来动态构建 PDA,但这里的关键问题是 PDA 在技术上是如何实现的。我认为手动编写每个转换函数可能是可能/直接的,但我并不完全确定。无论哪种方式都适合我。
解决方案
不可能创建一个 PDA 来生成这种树形数据结构,因为:
- PDA 具有有限数量的状态和堆栈符号,并且没有输出磁带。然而,这个 PDA 应该能够以某种方式表示(并提供)的树的数量是无限的。如果我们在面向对象的结构中考虑树的节点,那么可能有无数个不同的节点,因为节点的身份也由它所拥有的引用决定。这与 PDA 具有的有限符号集相冲突。如果作为替代方案,我们不会选择面向对象的输出,那么我们将一无所获:输入已经是树的序列化表示。
- 即使您将输出磁带添加到此自动机,使其成为重写系统,您也不会获得太多收益,因为输入已经以序列化格式表示树。如果我理解正确,您对序列化输出不感兴趣(如 JSON,因为输出顺序与输入顺序相同,这将是微不足道的),而是结构化输出(类似 JavaScript 的对象)。
因此,正如其他人也指出的那样,我们需要以某种方式放松规则。我们可以想到几种(组合)方法来做到这一点:
- 允许无限数量的状态,或
- 允许无限堆栈字母表,或
- 允许 PDA 访问堆栈的更多内容,而不仅仅是其顶部元素
- 让转换表引用堆栈顶部元素的特定属性,而不是整个元素 - 允许堆栈元素的其他属性的无限可能性,同时确保该特定属性的值属于有限集。
- 将树形建筑保持在 PDA 之外
- ...解决 PDA 与生成结构化树的要求不兼容的任何其他措施。
其中一些选项会推断出无限转换表,因此在这种情况下它不能是表,但可以是函数。
实施选择
1.对于通用引擎
考虑到您对树构建的关注以及对“堆栈、状态和转换”的要求,我选择了以下实现选择,有时还进行了简化:
- 我从上面的列表中选择策略 4
- 通用引擎将自己创建树。所以它只是在有限的意义上是通用的。它只能验证和生成树。
- 它将(仅)将转换表作为配置输入
- 它只有两个状态(布尔值),表示到目前为止是否一切正常。
- 只有在转换表中没有匹配的转换时,OK 状态才能从 true 变为 false
- 一旦 OK 状态为假,引擎甚至不会尝试找到转换,而是会忽略任何进一步的输入。
- 结果,转换表将不包括当前状态和未来状态部分,因为它们都被暗示为真(即“OK”)。
- 树将在堆栈中编码。堆栈元素的
data
属性将是用于识别转换的特殊属性。每个堆栈元素的children
属性将用于定义树,并且不被视为转换表所针对的堆栈字母表的一部分。 - 堆栈从一个元素开始。它的
children
属性将是引擎的输出。在每一步都可以查询这个输出,因为这个元素永远不会从堆栈中删除。 - 输入符号不能包含空字符串,该字符串保留用于指示输入流的结束。这允许引擎给出反馈是否是结束输入流的有效时刻。
- 当指示输入结束时,引擎要求其堆栈为“空”(只有 1 个条目)。
- 我假设输入符号不包含空格:空格在转换查找算法中用作分隔符
- 给定的转换表被转换为(散列)映射,以允许在给定当前状态和堆栈顶部的数据的情况下快速查找。此映射中的键是输入和堆栈数据值的串联,并且在这里选择空格作为分隔符。如果空格是输入符号中的有效字符,则应选择另一种编码机制来序列化此密钥对(例如 JSON),但我想保持简单。
- 引擎不需要获取输入符号集,也不需要获取堆栈符号集作为配置输入,因为这些将在转换表中隐含。
- 引擎的初始状态设置为 OK(可以通过简单的属性分配进行不同的设置,但这将是无用的,因为它会忽略输入)
2.对于具体的+/-输入格式
解决方案的更具体部分处理您在问题中给出的实际输入格式。一个函数会将一组类型转换为转换表,另一个函数将基于“+”和“-”字符对输入进行标记,但没有任何验证(没有符号检查,也没有符号长度检查......),因为无论如何,当调用引擎时,这一切都会显示为错误。
输入中的任何空白都将被忽略。
执行
// To peek the top element of a stack:
Array.prototype.peek = function () { return this[this.length - 1] };
function createParser(transitions) {
function createNode(data) {
return {
data,
children: []
};
}
function addChild(parentNode, data) {
const childNode = createNode(data);
parentNode.children.push(childNode);
return childNode;
}
let isOK = true; // It's a 2-state (boolean) engine. Transitions implicitly only apply to OK-state
const stack = [createNode("")]; // Stack is private, and always has Sentinel node with value ""
// Create a map for the transitions table, for faster lookup
// We use space as a delimiter for the key pair, assuming an input symbol does not include spaces.
const transMap = new Map(transitions.map(({whenInput, whenStack, thenPushValue}) =>
[whenInput + " " + whenStack, thenPushValue]
));
const parser = {
read(input) { // Returns true when parser can accept more input after this one
// Once the engine is in an error state, it will not process any further inputs
if (!isOK) {
return false;
}
// Consider the empty string as the end-of-input marker
if (input === "") {
// Even when state is OK, the stack should now also be "empty"
isOK &&= stack.length === 1;
return false; // Not an error condition, but indication that no more input is expected
}
// Transitions do not include state requirements, nor new state definitions.
// It is assumed that a transition can only occur in an OK state, and that all
// included transitions lead to an OK state.
const pushValue = transMap.get(input + " " + stack.peek().data);
if (pushValue === undefined) { // No matching transition in the table implies that state is not OK
isOK = false;
} else {
// As this is a two-state engine, with defined transitions only between OK states,
// each defined transition will affect the stack: so it's either a push or pop.
// A push is identified by the (non-empy) value to be pushed. An empty string denotes a pop.
if (pushValue) {
stack.push(addChild(stack.peek(), pushValue));
} else {
stack.pop();
}
}
return isOK;
},
isOK, // Expose the (boolean) state
output: stack[0].children // Don't expose the stack, but just the forest encoded in it
};
return parser;
}
function createTransition(whenInput, whenStack, thenPushValue) {
return {whenInput, whenStack, thenPushValue}; // First two values imply the third
}
// Functions specific for the input format in the question:
function createTransitionTable(types) {
// Specific to the input structure (with + and - tags) given in the question
// An empty string in the second column represents an empty stack
return [
// Transitions for opening tags: O(n²)
...["", ...types].flatMap(stack =>
types.map(type => createTransition("+" + type, stack, type))
),
// Transitions for closing tags
...types.map(type => createTransition("-" + type, type, ""))
];
}
function tokenizer(str) { // Could be a generator, but I went for a function-returning function
str = str.replace(/\s+/g, ""); // remove white space from input string
let current = 0; // Maintain state between `getNextToken` function calls
function getNextToken() {
const start = current++;
while (current < str.length && str[current] !== "+" && str[current] !== "-") {
current++;
}
const token = str.slice(start, current);
console.log("read", token); // For debugging
return token;
}
return getNextToken;
}
// Example input language, as defined in the question:
const types = ["aaa", "bbb", "ccc", "ddd", "eee"];
const transitionTable = createTransitionTable(types);
const parser = createParser(transitionTable);
// Example input for it:
const rawInput = `
+eee-eee
+aaa+bbb-bbb-aaa
+bbb+aaa+ccc+eee-eee-ccc+ccc-ccc+ddd+ccc+eee-eee-ccc-ddd-aaa-bbb`;
const getNextToken = tokenizer(rawInput);
// Process tokens
while (parser.read(getNextToken())) {}
console.log("Parser state is OK?: ", parser.isOK);
console.log("Parser output:\n");
console.log(JSON.stringify(parser.output, null, 3));
推荐阅读
- rest - 如何从 HERE API 检索建筑物高度信息?
- javascript - React-Native 简单存储错误:无法加载 RocksDB、SQLite 或 AsyncLocalStorage
- jwt - 使用 jjwt 进行 JWT 验证
- c++ - 如何从另一个头文件c ++调用函数
- javascript - 通过 Java Script 获取发布到操作页面的值
- javascript - 如何在 Angular (v6) 中动态地向材料表单添加验证?
- firebase - 在后端服务器中使用 Firebase 身份验证时如何避免命中率限制?
- javascript - es2018 中不区分大小写的字符串操作
- swift - 在 AKPlayer 中播放后尝试录制时 AudioKit 崩溃
- python - 提高文件下载速度