如何在JavaScript中实现编程语言。 第1部分:解析器

你好 我向您介绍了用于实施JavaScript编程语言的指南的业余翻译-PL教程


来自翻译


我们将创建自己的编程语言-λ语言 (原始语言 -λanguage)。 在创建过程中,我们将使用许多有趣的技术,例如递归下降,控件传递样式和基本的优化技术。 将创建两个版本的解释器-常规和CPS解释器,即JavaScript中的反编译器。


原著的作者是Mihai Bazon ,著名的UglifyJS库(一种用于最小化和格式化JS代码的工具)的作者。



参赛作品


如果您曾经编写过编译器或解释器,那么对您来说就没有什么新鲜的了。 但是,如果您使用正则表达式来解析类似于编程语言的内容,那么请阅读有关解析的部分。 让我们编写更少错误的代码!


本文按“从简单到复杂”的顺序分为几部分。 我不建议您跳过本文的某些部分,除非您对主题非常了解。 如果您不了解某些内容,可以随时返回。


我们要学习什么:


  • 什么是解析器,以及如何编写它。
  • 如何编写口译员。
  • 转移的续集及其重要性。
  • 编写一个(trans)编译器。
  • 延续转移风格。
  • 一些基本的代码优化技术。
  • 与JavaScript相比,我们的λ语言新功能示例。

与此同时,我将展示为什么Lisp是一种很棒的编程语言。 但是,我们正在使用的语言不是Lisp。 它具有更丰富的语法(每个人都知道的经典后缀表示法),类似于Scheme(宏除外)。 好的与否,但是宏是Lisp的主要功能-其他语言(除了Lisp的方言)无法做到的和它一样。 (是的,我知道SweetJS ...关闭,但不是。)


但是首先,让我们介绍一下我们的编程语言。



λ语言


在做任何事情之前,我们必须清楚地知道我们想做什么。 编写语言语法的描述会很好,但是我将使其变得更容易-编写一个简单程序的示例,因此这里是λ语言的一些示例:


#   println("Hello World!"); println(2 + 3 * 4); #       `lambda`  `λ` fib = lambda (n) if n < 2 then n else fib(n - 1) + fib(n - 2); println(fib(15)); print-range = λ(a, b) # `λ`     ,   `lambda` if a <= b then { # `then`  ,      print(a); if a + 1 <= b { print(", "); print-range(a + 1, b); } else println(""); #   }; print-range(1, 5); 

关于变量名的注意

请注意,标识符可能包含减号( print-range )。 这是一个品味问题。 我总是在运算符周围放置空格。 我真的不喜欢camelRegister和破折号胜过不可见的空格(“ _”)。 自己做某事时,您可以做的多么酷。


结论:


 Hello World! 14 610 1, 2, 3, 4, 5 

该语言看起来与JavaScript类似,但总体而言并非如此。 首先,没有语句,只有表达式。 一个表达式必须返回一个值,并且可以在另一个表达式的任何地方使用。 需要分号来分隔表达式的“序列”中的表达式。 花括号{}创建一个序列,该序列本身就是一个表达式,其值是该序列中的最后一个值。 以下程序是正确的:


 a = { fib(10); #    ,      fib(15) #        }; print(a); #  610 

使用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: [ //  : { type: "assign", operator: "=", left: { type: "var", value: "sum" }, right: { type: "lambda", vars: [ "a", "b" ], body: { //     "prog",  ,  //     ,  //     . type: "binary", operator: "+", left: { type: "var", value: "a" }, right: { type: "var", value: "b" } } } }, //  : { type: "call", func: { type: "var", value: "print" }, args: [{ type: "call", func: { type: "var", value: "sum" }, args: [ { type: "num", value: 1 }, { type: "num", value: 2 } ] }] } ] } 

创建解析器的主要困难是正确组织代码的困难。 与从字符串中读取字符相比,解析器应该在更高的层次上工作。 一些降低代码复杂性的准则:


  • 编写很多小功能。 在每个功能中,都要做一件事并做好。
  • 不要尝试使用正则表达式进行解析。 他们只是不工作。 它们在词法分析器中可能很有用,但为简单起见,我们将不使用它们。
  • 不要试图猜测。 如果不确定如何解析某些内容,请引发一个包含错误位置(行和列)的异常。

为了使代码更简单,我们将其分为三部分,然后又分为许多小功能:




字符流


这是最简单的部分。 我们将创建一个流对象,该对象将表示从字符串中顺序读取字符的操作。 它包含四个功能:


  • 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()函数的返回值将是标记。 令牌是具有两个属性的typetypevalue 。 以下是令牌的一些示例:


 { type: "punc", value: "(" } // . : , ,     . . { type: "num", value: 5 } //  { type: "str", value: "Hello World!" } //  { type: "kw", value: "lambda" } //   { type: "var", value: "a" } //  { type: "op", 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!" } 

truefalsebool ):

 true false 

 { type: "bool", value: true } { type: "bool", value: false } 

标识符( var ):

 foo 

 { type: "var", value: "foo" } 

函数( lambda ):

 lambda (x) 10 #  λ (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函数来执行此操作,该函数采用以下参数: startstopseparatorparser函数,该函数分别解析每个元素。 在这种情况下,我们使用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; //      a.push(parser()); } skip_punc(stop); return a; } 

如您所见,它使用了更多功能: is_puncskip_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节点。


 //     FALSE   , //     . var FALSE = { type: "bool", value: false }; 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 }; } 

这是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) // (*); var binary = { type : tok.value == "=" ? "assign" : "binary", operator : tok.value, left : left, right : right }; return maybe_binary(binary, my_prec); } } return left; } 

, , , 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部分:翻译

Source: https://habr.com/ru/post/zh-CN443630/


All Articles