你好 我向您介绍了用于实施JavaScript编程语言的指南的业余翻译-PL教程 。
来自翻译
我们将创建自己的编程语言-λ语言 (原始语言 -λanguage)。 在创建过程中,我们将使用许多有趣的技术,例如递归下降,控件传递样式和基本的优化技术。 将创建两个版本的解释器-常规和CPS解释器,即JavaScript中的反编译器。
原著的作者是Mihai Bazon ,著名的UglifyJS库(一种用于最小化和格式化JS代码的工具)的作者。
参赛作品
如果您曾经编写过编译器或解释器,那么对您来说就没有什么新鲜的了。 但是,如果您使用正则表达式来解析类似于编程语言的内容,那么请阅读有关解析的部分。 让我们编写更少错误的代码!
本文按“从简单到复杂”的顺序分为几部分。 我不建议您跳过本文的某些部分,除非您对主题非常了解。 如果您不了解某些内容,可以随时返回。
我们要学习什么:
- 什么是解析器,以及如何编写它。
- 如何编写口译员。
- 转移的续集及其重要性。
- 编写一个(trans)编译器。
- 延续转移风格。
- 一些基本的代码优化技术。
- 与JavaScript相比,我们的λ语言新功能示例。
与此同时,我将展示为什么Lisp是一种很棒的编程语言。 但是,我们正在使用的语言不是Lisp。 它具有更丰富的语法(每个人都知道的经典后缀表示法),类似于Scheme(宏除外)。 好的与否,但是宏是Lisp的主要功能-其他语言(除了Lisp的方言)无法做到的和它一样。 (是的,我知道SweetJS ...关闭,但不是。)
但是首先,让我们介绍一下我们的编程语言。
λ语言
在做任何事情之前,我们必须清楚地知道我们想做什么。 编写语言语法的描述会很好,但是我将使其变得更容易-编写一个简单程序的示例,因此这里是λ语言的一些示例:
关于变量名的注意请注意,标识符可能包含减号( print-range
)。 这是一个品味问题。 我总是在运算符周围放置空格。 我真的不喜欢camelRegister和破折号胜过不可见的空格(“ _”)。 自己做某事时,您可以做的多么酷。
结论:
Hello World! 14 610 1, 2, 3, 4, 5
该语言看起来与JavaScript类似,但总体而言并非如此。 首先,没有语句,只有表达式。 一个表达式必须返回一个值,并且可以在另一个表达式的任何地方使用。 需要分号来分隔表达式的“序列”中的表达式。 花括号{
和}
创建一个序列,该序列本身就是一个表达式,其值是该序列中的最后一个值。 以下程序是正确的:
a = { fib(10);
使用lambda
或λ
关键字创建函数。 之后,括号中是参数名称(可能为空)列表,以逗号分隔。 函数的主体是一个表达式,但可以包含在序列{...}
。 还值得注意的是,没有return
关键字。 该函数返回最后一个表达式的值。
另外,没有var
。 为了添加变量,您可以使用JavaScript程序员称为IIFE
。 使用lambda
,将变量声明为参数。 对于变量,作用域是一个函数,函数是闭包,如JavaScript [ 翻译:直至ES6。]。
即使是一个表达。 JavaScript使用三元运算符执行此操作:
a = foo() ? bar() : baz(); // JavaScript a = if foo() then bar() else baz();
如上所示,如果分支是序列( {...}
), then
关键字是可选的。 在另一种情况下,这是必要的。 如果存在替代分支,则使用else
。 再一次, then
表达式作为主体,但是您可以使用{...}
将多个表达式组合为一个。 如果不存在else
并且条件为false
,则所有if
的结果为false
。 值得注意的是, false
是表示值的关键字,该值是λ语言中唯一的错误值:
if foo() then print("OK");
仅当foo()
的结果不为 false
才会输出OK
。 另外,还有一个关键字true
,但是绝对不是false
所有内容(在JavaScript中,运算符===
)在分支中都将视为true
(包括数字0
和空字符串""
)。
请注意,在if
,表达式之间不需要括号。 如果添加它们,那将不是一个错误,因为(
表达式开始,但是它们只是多余的。
即使用括号括起来,也可以解析整个程序,因此应添加;
在每个表达式之后。 最后一个表达式是一个例外。
太好了,这是我们的小λ语言。 他并不完美。 语法看起来很漂亮,但是有缺陷。 有许多缺少的功能,例如对象和数组。 我们不关注它们,因为它们不是我们编程语言的基础。
接下来,我们将为我们的语言编写一个解析器。
代码转换为AST
创建解析器是一项艰巨的任务。 本质上,它应该花费一段代码并将其转换为AST(抽象语法树)。 AST是内存中程序的结构化表示,是抽象的,因为它不包含有关代码的完整信息,而仅包含语义。 说明AST在单独的部分中。
例如,我们有以下代码:
sum = lambda(a, b) { a + b; }; print(sum(1, 2));
我们的解析器将生成一棵树作为JavaScript对象:
{ type: "prog", prog: [
创建解析器的主要困难是正确组织代码的困难。 与从字符串中读取字符相比,解析器应该在更高的层次上工作。 一些降低代码复杂性的准则:
- 编写很多小功能。 在每个功能中,都要做一件事并做好。
- 不要尝试使用正则表达式进行解析。 他们只是不工作。 它们在词法分析器中可能很有用,但为简单起见,我们将不使用它们。
- 不要试图猜测。 如果不确定如何解析某些内容,请引发一个包含错误位置(行和列)的异常。
为了使代码更简单,我们将其分为三部分,然后又分为许多小功能:
字符流
这是最简单的部分。 我们将创建一个流对象,该对象将表示从字符串中顺序读取字符的操作。 它包含四个功能:
peek()
-返回下一个字符,而不从流中提取它。next()
-返回下一个字符,将其从流中提取出来。eof()
-如果流中没有更多字符,则返回true
。croak(msg)
-引发包含消息( msg
)和流中当前位置的异常。
需要最后一个函数,以便您可以简单地引发一个包含错误位置的异常。
这是此对象的完整代码(我们称其为InputStream
)。 它足够小,因此您不会遇到任何问题:
function InputStream(input) { var pos = 0, line = 1, col = 0; return { next : next, peek : peek, eof : eof, croak : croak, }; function next() { var ch = input.charAt(pos++); if (ch == "\n") line++, col = 0; else col++; return ch; } function peek() { return input.charAt(pos); } function eof() { return peek() == ""; } function croak(msg) { throw new Error(msg + " (" + line + ":" + col + ")"); } }
请注意,这不是普通对象(通过new
创建)。 要获得此对象,您需要: var stream = InputStream(string)
。
接下来,我们将编写以下抽象级别: 令牌流(令牌) 。
令牌流(令牌)
标记器(lexer)使用字符流并返回具有相同接口的对象,但是peek()
/ next()
函数的返回值将是标记。 令牌是具有两个属性的type
: type
, value
。 以下是令牌的一些示例:
{ type: "punc", value: "(" }
只需跳过空格(空格,制表符,换行符)和注释。
要编写令牌生成器,我们需要仔细研究一下我们的语言。 这个想法是要注意,根据当前字符( input.peek()
),我们可以决定读取哪个标记:
- 首先,跳过空格。
- 如果为
input.eof()
,则返回null
。 - 如果它是
#
字符,则将所有字符跳过到该行的末尾(并返回下一个标记)。 - 如果这是引号,则读取字符串。
- 如果这是一个数字,请阅读该数字。
- 如果是字母,则我们读取单词,然后返回标识符或关键字。
- 如果这是特殊字符之一,则返回相应的标记。
- 如果这是运算符的符号之一,则我们返回相应的令牌。
- 如果以上都不适用,则使用
input.croak()
引发异常。
我们将拥有read_next
函数,这是分词器的主要功能:
function read_next() { read_while(is_whitespace); if (input.eof()) return null; var ch = input.peek(); if (ch == "#") { skip_comment(); return read_next(); } if (ch == '"') return read_string(); if (is_digit(ch)) return read_number(); if (is_id_start(ch)) return read_ident(); if (is_punc(ch)) return { type : "punc", value : input.next() }; if (is_op_char(ch)) return { type : "op", value : read_while(is_op_char) }; input.croak("Can't handle character: " + ch); }
在这里,您可以看到许多其他函数,它们返回不同类型的令牌,例如read_string()
, read_number()
等。...它们被放置在单独的函数中,因此代码看起来更简单,更漂亮。
另外,有趣的是我们不会一次删除所有符号:每次解析器要求下一个令牌时,我们都会读取一个令牌。 如果发生某种错误,我们甚至不会阅读所有字符。
read_ident()
将读取可能是标识符( is_id()
)一部分的所有字符。 标识符必须以字母λ
或_
开头,并且可以包含相同的字符,数字或以下任意一项: ?!-<>=
。 因此, foo-bar
不会被读取为三个令牌,而将被读取为一个( var
token)。 为了能够使用is-pair?
名称定义函数,这是否必要is-pair?
或string>=
(对不起,我是Lisper)。
另外, read_ident()
将检查已知关键字列表中是否存在标识符,如果存在,则将返回kw
令牌而不是var
令牌。
我认为代码说明了一切,因此这是我们语言的现成标记器:
整个代码 function TokenStream(input) { var current = null; var keywords = " if then else lambda λ true false "; return { next : next, peek : peek, eof : eof, croak : input.croak }; function is_keyword(x) { return keywords.indexOf(" " + x + " ") >= 0; } function is_digit(ch) { return /[0-9]/i.test(ch); } function is_id_start(ch) { return /[a-zλ_]/i.test(ch); } function is_id(ch) { return is_id_start(ch) || "?!-<>=0123456789".indexOf(ch) >= 0; } function is_op_char(ch) { return "+-*/%=&|<>!".indexOf(ch) >= 0; } function is_punc(ch) { return ",;(){}[]".indexOf(ch) >= 0; } function is_whitespace(ch) { return " \t\n".indexOf(ch) >= 0; } function read_while(predicate) { var str = ""; while (!input.eof() && predicate(input.peek())) str += input.next(); return str; } function read_number() { var has_dot = false; var number = read_while(function(ch){ if (ch == ".") { if (has_dot) return false; has_dot = true; return true; } return is_digit(ch); }); return { type: "num", value: parseFloat(number) }; } function read_ident() { var id = read_while(is_id); return { type : is_keyword(id) ? "kw" : "var", value : id }; } function read_escaped(end) { var escaped = false, str = ""; input.next(); while (!input.eof()) { var ch = input.next(); if (escaped) { str += ch; escaped = false; } else if (ch == "\\") { escaped = true; } else if (ch == end) { break; } else { str += ch; } } return str; } function read_string() { return { type: "str", value: read_escaped('"') }; } function skip_comment() { read_while(function(ch){ return ch != "\n" }); input.next(); } function read_next() { read_while(is_whitespace); if (input.eof()) return null; var ch = input.peek(); if (ch == "#") { skip_comment(); return read_next(); } if (ch == '"') return read_string(); if (is_digit(ch)) return read_number(); if (is_id_start(ch)) return read_ident(); if (is_punc(ch)) return { type : "punc", value : input.next() }; if (is_op_char(ch)) return { type : "op", value : read_while(is_op_char) }; input.croak("Can't handle character: " + ch); } function peek() { return current || (current = read_next()); } function next() { var tok = current; current = null; return tok || read_next(); } function eof() { return peek() == null; } }
next()
函数并不总是调用read_next()
,因为以前可能已经读取了一个令牌(使用peek()
函数)。 为此,我们有一个包含当前标记的current
变量。- 常规表示法仅支持十进制数字(不支持1E5、0x等)。 但是,如果我们想增加他们的支持,我们只会更改
read_number()
。 - 与JavaScript不同,唯一不能在字符串中转义的字符是引号和反斜杠。 行可以包含换行符,制表符和其他任何内容。 我们不解释诸如
\n
, \t
等标准组合。...重做( read_string()
)非常容易。
现在我们有了功能强大的工具,可以轻松编写解析器 ,但是首先,我建议您查看AST描述 。
AST说明
如上所述,解析器将构建一个结构,以显示程序的语义。 AST由节点组成。 每个节点都是一个常规JavaScript对象,该对象具有type
属性,该属性确定节点的类型以及取决于该类型的其他信息。
型式 | 结构形式 |
---|
数 | { type: "num", value: NUMBER } |
力量 | { type: "str", value: STRING } |
布尔 | { type: "bool", value: true or false } |
变种 | { type: "var", value: NAME } |
拉姆达 | { type: "lambda", vars: [ NAME... ], body: AST } |
叫 | { type: "call", func: AST, args: [ AST... ] } |
如果 | { type: "if", cond: AST, then: AST, else: AST } |
分配 | { type: "assign", operator: "=", left: AST, right: AST } |
二元 | { type: "binary", operator: OPERATOR, left: AST, right: AST } |
编 | { type: "prog", prog: [ AST... ] } |
让 | { type: "let", vars: [ VARS... ], body: AST } |
例子数字( num
):
123.5
{ type: "num", value: 123.5 }
行( str
):
"Hello World"
{ type: "str", value: "Hello World!" }
true
与false
( bool
):
true false
{ type: "bool", value: true } { type: "bool", value: false }
标识符( var
):
foo
{ type: "var", value: "foo" }
函数( lambda
):
lambda (x) 10
{ type: "lambda", vars: [ "x" ], body: { type: "num", value: 10 } }
稍后,我们将添加一个可选的参数name
以支持带有名称的函数,但解析器的第一个版本将不支持它们。
函数调用( call
):
foo(a, 1)
{ "type": "call", "func": { "type": "var", "value": "foo" }, "args": [ { "type": "var", "value": "a" }, { "type": "num", "value": 1 } ] }
分支( if
):
if foo then bar else baz
{ "type": "if", "cond": { "type": "var", "value": "foo" }, "then": { "type": "var", "value": "bar" }, "else": { "type": "var", "value": "baz" } }
没有else
:
if foo then bar
{ "type": "if", "cond": { "type": "var", "value": "foo" }, "then": { "type": "var", "value": "bar" } }
作业:
a = 10
{ "type": "assign", "operator": "=", "left": { "type": "var", "value": "a" }, "right": { "type": "num", "value": 10 } }
二进制运算符( binary
):
x + y * z
{ "type": "binary", "operator": "+", "left": { "type": "var", "value": "x" }, "right": { "type": "binary", "operator": "*", "left": { "type": "var", "value": "y" }, "right": { "type": "var", "value": "z" } } }
序列( prog
):
{ a = 5; b = a * 2; a + b; }
{ "type": "prog", "prog": [ { "type": "assign", "operator": "=", "left": { "type": "var", "value": "a" }, "right": { "type": "num", "value": 5 } }, { "type": "assign", "operator": "=", "left": { "type": "var", "value": "b" }, "right": { "type": "binary", "operator": "*", "left": { "type": "var", "value": "a" }, "right": { "type": "num", "value": 2 } } }, { "type": "binary", "operator": "+", "left": { "type": "var", "value": "a" }, "right": { "type": "var", "value": "b" } } ] }
包含在块中的变量( let
):
let (a = 10, b = a * 10) { a + b; }
{ "type": "let", "vars": [ { "name": "a", "def": { "type": "num", "value": 10 } }, { "name": "b", "def": { "type": "binary", "operator": "*", "left": { "type": "var", "value": "a" }, "right": { "type": "num", "value": 10 } } } ], "body": { "type": "binary", "operator": "+", "left": { "type": "var", "value": "a" }, "right": { "type": "var", "value": "b" } } }
解析器的第一个版本不支持这种类型的节点;稍后我们将添加它。
解析器
解析器将构建上述树。
由于我们在令牌生成器中所做的工作,因此解析器可以处理令牌流而不是字符流。 这里还有许多其他功能可以简化结构。 我们将讨论主要的。 让我们从一个高级的解析器功能开始:
function parse_lambda() { return { type: "lambda", vars: delimited("(", ")", ",", parse_varname), body: parse_expression() }; }
当已经从令牌流中获取了lambda
关键字时,将调用此函数,因此我们只需要获取参数的名称即可。 但是由于它们在方括号中并用逗号分隔,因此我们将使用定delimited
函数来执行此操作,该函数采用以下参数: start
, stop
, separator
, parser
函数,该函数分别解析每个元素。 在这种情况下,我们使用parse_varname
函数,如果它注意到一些看起来不像变量的函数, parse_varname
引发错误。 该函数的主体是一个表达式,因此我们可以通过parse_expression
获得它。
delimited
功能为较低级别:
function delimited(start, stop, separator, parser) { var a = [], first = true; skip_punc(start); while (!input.eof()) { if (is_punc(stop)) break; if (first) first = false; else skip_punc(separator); if (is_punc(stop)) break;
如您所见,它使用了更多功能: is_punc
和skip_punc
。 如果当前标记是给定的标点符号(不提取它),则第一个返回true
,而skip_punc
将检查当前标记是否是给定的标点字符并提取它(否则抛出异常)。
解析整个程序的函数似乎是最简单的:
function parse_toplevel() { var prog = []; while (!input.eof()) { prog.push(parse_expression()); if (!input.eof()) skip_punc(";"); } return { type: "prog", prog: prog }; }
由于我们只有表达式,因此我们只需调用parse_expression()
并读取表达式,直到读取所有内容。 使用skip_punc(";")
,我们可以做到;
每个表达式后必须输入。
另一个简单的示例是parse_if()
:
function parse_if() { skip_kw("if"); var cond = parse_expression(); if (!is_punc("{")) skip_kw("then"); var then = parse_expression(); var ret = { type: "if", cond: cond, then: then }; if (is_kw("else")) { input.next(); ret.else = parse_expression(); } return ret; }
她跳过if
关键字(如果当前令牌不是if
关键字, if
抛出异常),使用parse_expression()
读取条件。 如果符号{
没有更进一步,则需要then关键字(如果没有它,语法看起来会不太好)。 分支只是表达式,因此我们再次对其使用parse_expression()
。 else
分支是可选的,因此我们在解析关键字之前先检查该关键字的存在。
通过许多小的功能,我们可以使代码简单。 我们编写解析器的方式几乎与使用高级语言来解析语法的方式一样。 所有这些函数都是“相互递归”的,也就是说,我们有parse_atom()
,它根据当前标记调用其他函数。 其中之一是parse_if()
(在当前标记为if
调用),然后依次调用parse_expression()
。 但是parse_expression()
调用parse_atom()
。 由于函数之一总是提取至少一个令牌,因此没有无限递归。
这种解析方法称为“ 递归下降法” ,实际上,这是最容易编写的。
下层: parse_atom()
和parse_expression()
parse_atom()
函数调用另一个函数,具体取决于当前令牌:
function parse_atom() { return maybe_call(function(){ if (is_punc("(")) { input.next(); var exp = parse_expression(); skip_punc(")"); return exp; } if (is_punc("{")) return parse_prog(); if (is_kw("if")) return parse_if(); if (is_kw("true") || is_kw("false")) return parse_bool(); if (is_kw("lambda") || is_kw("λ")) { input.next(); return parse_lambda(); } var tok = input.next(); if (tok.type == "var" || tok.type == "num" || tok.type == "str") return tok; unexpected(); }); }
当她看到parse_expression()
括号时,应该加上括号表达式,因此,跳过该方括号,该函数将调用parse_expression()
并希望此后跳过该右方括号。 如果她看到某种关键字,则将调用相应的函数。 如果她看到一个常量或标识符,则按原样返回它。 如果什么都没有发生,它将调用unexpected()
,这将引发异常。
当看到{
,她调用parse_prog
来解析表达式序列。 另外, parse_prog
了一个简单的优化:如果{
和}
之间没有表达式,则返回false
;如果只有一个表达式,则仅返回它。 否则,将返回带有表达式数组的prog
节点。
这是parse_expression()
函数。 与parse_atom()
不同,它将使用maybe_binary()
解析尽可能多的表达式:
function parse_expression() { return maybe_call(function(){ return maybe_binary(parse_atom(), 0); }); }
功能可能maybe_*
这些函数检查表达式之后的内容,并确定是将表达式包装在其节点中还是直接返回该节点。
maybe_call()
函数非常简单:它获得了一个解析当前表达式的函数,如果遇到(
在表达式之后,它将自身包装在call
。请注意delimited()
如何适合解析参数列表:
function maybe_call(expr) { expr = expr(); return is_punc("(") ? parse_call(expr) : expr; } function parse_call(func) { return { type: "call", func: func, args: delimited("(", ")", ",", parse_expression) }; }
操作员优先
maybe_binary(left, my_prec)
函数用于组合诸如1 + 2 * 3
表达式。 , , , :
var PRECEDENCE = { "=": 1, "||": 2, "&&": 3, "<": 7, ">": 7, "<=": 7, ">=": 7, "==": 7, "!=": 7, "+": 10, "-": 10, "*": 20, "/": 20, "%": 20, };
, *
"", +
, , 1 + 2 * 3
(1 + (2 * 3))
((1 + 2) * 3)
.
, ( read_atom
) maybe_binary()
( ), ( my_prec
). maybe_binary
, . , , .
, , , binary
, , , (*):
function maybe_binary(left, my_prec) { var tok = is_op(); if (tok) { var his_prec = PRECEDENCE[tok.value]; if (his_prec > my_prec) { input.next(); var right = maybe_binary(parse_atom(), his_prec)
, , , maybe_binary
, ( my_prec
), , , . - , (, ), .
, , my_prec
0, binary
( assign
=
).
, .
var FALSE = { type: "bool", value: false }; function parse(input) { var PRECEDENCE = { "=": 1, "||": 2, "&&": 3, "<": 7, ">": 7, "<=": 7, ">=": 7, "==": 7, "!=": 7, "+": 10, "-": 10, "*": 20, "/": 20, "%": 20, }; return parse_toplevel(); function is_punc(ch) { var tok = input.peek(); return tok && tok.type == "punc" && (!ch || tok.value == ch) && tok; } function is_kw(kw) { var tok = input.peek(); return tok && tok.type == "kw" && (!kw || tok.value == kw) && tok; } function is_op(op) { var tok = input.peek(); return tok && tok.type == "op" && (!op || tok.value == op) && tok; } function skip_punc(ch) { if (is_punc(ch)) input.next(); else input.croak("Expecting punctuation: \"" + ch + "\""); } function skip_kw(kw) { if (is_kw(kw)) input.next(); else input.croak("Expecting keyword: \"" + kw + "\""); } function skip_op(op) { if (is_op(op)) input.next(); else input.croak("Expecting operator: \"" + op + "\""); } function unexpected() { input.croak("Unexpected token: " + JSON.stringify(input.peek())); } function maybe_binary(left, my_prec) { var tok = is_op(); if (tok) { var his_prec = PRECEDENCE[tok.value]; if (his_prec > my_prec) { input.next(); return maybe_binary({ type : tok.value == "=" ? "assign" : "binary", operator : tok.value, left : left, right : maybe_binary(parse_atom(), his_prec) }, my_prec); } } return left; } function delimited(start, stop, separator, parser) { var a = [], first = true; skip_punc(start); while (!input.eof()) { if (is_punc(stop)) break; if (first) first = false; else skip_punc(separator); if (is_punc(stop)) break; a.push(parser()); } skip_punc(stop); return a; } function parse_call(func) { return { type: "call", func: func, args: delimited("(", ")", ",", parse_expression), }; } function parse_varname() { var name = input.next(); if (name.type != "var") input.croak("Expecting variable name"); return name.value; } function parse_if() { skip_kw("if"); var cond = parse_expression(); if (!is_punc("{")) skip_kw("then"); var then = parse_expression(); var ret = { type: "if", cond: cond, then: then, }; if (is_kw("else")) { input.next(); ret.else = parse_expression(); } return ret; } function parse_lambda() { return { type: "lambda", vars: delimited("(", ")", ",", parse_varname), body: parse_expression() }; } function parse_bool() { return { type : "bool", value : input.next().value == "true" }; } function maybe_call(expr) { expr = expr(); return is_punc("(") ? parse_call(expr) : expr; } function parse_atom() { return maybe_call(function(){ if (is_punc("(")) { input.next(); var exp = parse_expression(); skip_punc(")"); return exp; } if (is_punc("{")) return parse_prog(); if (is_kw("if")) return parse_if(); if (is_kw("true") || is_kw("false")) return parse_bool(); if (is_kw("lambda") || is_kw("λ")) { input.next(); return parse_lambda(); } var tok = input.next(); if (tok.type == "var" || tok.type == "num" || tok.type == "str") return tok; unexpected(); }); } function parse_toplevel() { var prog = []; while (!input.eof()) { prog.push(parse_expression()); if (!input.eof()) skip_punc(";"); } return { type: "prog", prog: prog }; } function parse_prog() { var prog = delimited("{", "}", ";", parse_expression); if (prog.length == 0) return FALSE; if (prog.length == 1) return prog[0]; return { type: "prog", prog: prog }; } function parse_expression() { return maybe_call(function(){ return maybe_binary(parse_atom(), 0); }); } }
Marijn Haverbeke, parse-js (Common Lisp), , . , , JS, .
: JavaScript. 第2部分:翻译