JavaScript - 解析 S-expressions
Lisp 编程语言家族是以 S-expressions 为基础构建的。在本文中,您将学习制作一个简单的 S-expression 解析器的步骤。这可以作为 Lisp 解析器的基础。
Lisp 是最容易实现的语言,而创建解析器是第一步。我们可以使用 parser generator 来实现,但自己编写解析器更简单。我们将使用 JavaScript。
什么是 S-expressions?
为了定义嵌套的列表数据结构,我们使用 s-expressions,它们通常在 Lisp 和其他函数式编程语言中使用。一个 s-expression 可以是一个 atom,或者一系列 s-expressions。
如果您不了解 Lisp 语言,S-expressions 看起来像这样 −
(+ (second (list "xxx" 10)) 20)
这是一种数据格式,其中一切都由 atom 或用括号包围的列表组成(来自其他列表的 atom 由空格分隔)。
像 JSON 一样,S-expressions 可以有多种数据类型。数字、字符串和符号(不带引号)可以在多种语言中表示变量名。
此外,您可以使用特定的点运算符来形成像下面的 pair。
(1 . b)
列表可以表示为 doted pairs(这意味着它是一个 linked list 数据结构)。
这是一个列表 −
(1 2 3 4)
它可以写成 −
(1 . (2 . (3 . (4 . Nil))))
特殊符号 "nil" 表示空列表的结尾。这种格式允许您生成任何 binary tree。不过,为了避免复杂化,我们的解析器将不使用这种 doted notation。
S-expressions 的用途是什么?
S-expressions 用于创建 Lisp 代码,也可以用于通信数据。
它们也存在于 WebAssembly 的文本版本中。可能是因为解析器简单,而且您不必创建自己的格式。与其使用 JSON,不如用它们在服务器和浏览器之间通信。
JavaScript 中的逐步 S-expression 解析器
以下是 s-expression 解析器需要遵循的步骤 −
标记化输入:首先,将输入字符串分成 tokens,这些 tokens 可以是括号 (,) 或 symbols。
递归解析:递归处理 tokens 以创建结构。当找到开括号时,生成一个新列表。闭括号表示当前列表的结束。
基本情况:Symbols(如整数或单词)作为值返回,但列表使用括号内的表达式创建。
示例
以下代码将输入字符串转换为可读的 token(符号、整数和括号)。parse() 方法持续循环遍历每个 token。当检测到 ( 时,它创建一个新列表。当找到 ) 时,它完成该列表。数字被解析为 JavaScript 数字;其他一切都被解释为符号(字符串)。
// 将输入字符串分词为 S-expression token 的函数
function tokenize(input) {
return input
// 在 '(' 周围添加空格
.replace(/\(/g, ' ( ')
// 在 ')' 周围添加空格
.replace(/\)/g, ' ) ')
.trim()
// 按空白字符分割
.split(/\s+/);
}
// 递归函数,将 token 解析为 S-expression
function parse(tokens) {
if (tokens.length === 0) {
throw new Error("意外的输入结束");
}
// 获取下一个 token
let token = tokens.shift();
// 开始一个新列表
if (token === '(') {
let list = [];
// 处理直到遇到闭合括号
while (tokens[0] !== ')') {
// 递归解析内部表达式
list.push(parse(tokens));
}
tokens.shift(); // 移除闭合的 ')'
return list;
} else if (token === ')') {
throw new Error("意外的 ')'");
} else {
// 返回一个 atom(符号或数字)
return atom(token);
}
}
// 判断 token 是数字还是符号的函数
function atom(token) {
let number = Number(token);
if (!isNaN(number)) {
// 如果是数字,返回它
return number;
} else {
// 否则,将其作为符号返回
return token;
}
}
// 使用示例
let input = "(+ 1 (* 2 3))";
// 对输入进行分词
let tokens = tokenize(input);
// 将 token 解析为 AST(抽象语法树)
let ast = parse(tokens);
console.log(ast);
输出
如果使用上述输入运行以上代码,输出将为 −
["+", 1, ["*", 2, 3]]