实现一门超简单的编程语言最快需要多久?

这是宫文学老师的第一节课程,本次课程的内容读完,并没有完全清楚我自己要做什么,该怎么做,经过读配套的原码,终于明白需要做一些什么事情,此处仅作为记录,梳理思路。 以下为正文:

0x00

本次课程需要做的事情是进行一次简单的语法分析,将输入的 Token 数组,转换成一个语法树,紧接着处理函数与函数调用之间的关系,让函数与函数调用建立联系,最后在运行生成的语法树。(文中我使用的 Java 语言去实现的)

首先,来看一下我们需要解析的代码段内容:

function sayHello() {
  println("Hello World!");
}
sayHello();

以上代码,可以很简单的读懂,定义了一个函数 sayHello, 在这个函数中,调用了内置函数 println , 用来输出 Hello World! 这个字符串,在代码的最后,调用了这个定义的 sayHello 函数。

0x01

首先,我们手动将这个代码段拆分为 Token 数组。对于 Token 来说,有几种类型:

  • 关键字 TokenKind.KeyWord , 例如 function
  • 标识符 TokenKind.Identifier ,例如 sayHello, println
  • 字符串 TokenKind.StringLiteral,例如 “Hello World!”
  • 分隔符 TokenKind.Separator , 例如 空格,{}, ;
  • 终止符 TokenKind.EOF ,在 Token 数组最末尾,表示代码结束

将上面的代码段拆分为 Token 数组后就会变成下面这样:

static Token[] tokenArray = new Token[]{
  new Token(TokenKind.KeyWord, "function"),
  new Token(TokenKind.Identifier, "sayHello"),
  new Token(TokenKind.Separator, "("),
  new Token(TokenKind.Separator, ")"),
  new Token(TokenKind.Separator, "{"),
  new Token(TokenKind.Identifier, "println"),
  new Token(TokenKind.Separator, "("),
  new Token(TokenKind.StringLiteral, "Hello World!"),
  new Token(TokenKind.Separator, ")"),
  new Token(TokenKind.Separator, ";"),
  new Token(TokenKind.Separator, "}"),
  new Token(TokenKind.Identifier, "sayHello"),
  new Token(TokenKind.Separator, "("),
  new Token(TokenKind.Separator, ")"),
  new Token(TokenKind.Separator, ";"),
  new Token(TokenKind.EOF, "")
};

0x02

在回到语法解析,需要遍历整个 Token 列表,来生成语法树。语法树是把整个程序代码,按照一定的规则进行推导,形成一个树型结构。

回到本例中,为简化理解这个过程,我们的代码中,只包含简单的函数、函数调用的代码,以及系统打印函数 println 的调用。在我解析的代码片段中,使用 ; 来表示一行代码结束。

  1. 简单的函数表示的是, 以关键字 function 开头,跟函数名,并且函数不包含参数。在函数的函数体中,仅可包含任意多个函数的调用(代码中定义的函数,以及系统打印函数 println 的调用)。 函数的函数体包含在分割符 { } 中问。函数名称后需要跟分隔符 ( )

  2. 系统打印函数 println 的定义为println(String... args) ,即我们在调用这个函数的时候,可以传递任意多个字符串给它,用于在控制台进行打印输出。调用示例如下:

    println(); // 不输出任意值
    println("Hello World !"); // 传递一个参数, 打印结果为: Hello World !
    println("Hello", "World", "!")// 传递多个参数, 打印结果为: Hello World !
    

注意,在本例中, "Hello World!" 在转成 Token 的时候,把 "" 去掉,得到如下结果

new Token(TokenKind.StringLiteral, "Hello World!"),

0x03

经过上面的分析,能够很清楚明白的知道,语法解析程序需要干什么事情。

拿到 Token 数组后,需要从头遍历每一个 Token , 对于一段代码,需要知道当前代码应该按照函数定义来解析还是函数调用来解析, 要解决这个问题很简单,那就是一种一种试,先把代码给到解析程序,先按照 函数定义 来解析,如果解析的过程中,发现他不是函数定义, 就继续下一种可能,即按 函数调用 来解析。如果依然不是,在本例中,就需要报错,提示代码写错了。

  • 函数定义

    以关键字 function 开头, 后面会跟一个标识符来作为函数名,在本例中,定义函数中不支持参数,因此在函数名后会紧接着会跟 (), 在后面便是由 {} 括起来的函数体内容,当然函数体里面可以为空,比时就会只剩下 {}

  • 函数调用

    以标识符开头, 这个标识符为要调用函数名称,紧接着是(,在它后面可能会跟任意多个字符串(仅在系统打印函数中使用),最后会跟一个 ),在代码的最后,还需要有 ; 号来结尾。

  • 函数体

    如上面第二节描述,函数体中包含多个函数调用,直接向后进行遍历 Token,按函数调用方式持续解析,直到下一个 Token 为 } 结束。

如果在上述解析过程中,出现错误,可直接抛出异常。

从上面的描述,可以使用 EBNF 格式,进行更加专业的表达:

prog = (functionDecl | functionCall)* 
functionDecl: "function" Identifier "(" ")" functionBody 
functionBody: "{" functionCall* "}"
functionCall: Identifier "(" parameterList? ")"
parameterList: StringLiteral ("," StringLiteral)*

0x04 代码实现

根据上面的描述,程序代码会循环遍历所有代码,并尝试按照函数定义、函数调用两种规则去解析代码,构建语法树,因此最上层代码如下:

while (true) { 
  // 1. 先按照函数定义解析
  Statement statement = this.parseFunctionDecl();
  if (statement != null) {
    // 解析到函数定义后,存储起来,进行后面的 Token 解析
    statements.add(statement);
    continue;
  }
  // 2. 函数定义解析失败后,尝试按函数调用解析
  statement = this.parseFunctionCall();
  if (statement != null) {
    // 解析到函数调用后,存储起来,
    statements.add(statement);
    continue;
  }
  // 3. 既不是函数定义,也不是函数调用, 会解析不成功,终止本次解析
  if (statement == null) {
    break;
  }
}

接下来,进行函数定义的解析,代码如下:

if (token.kind == TokenKind.KeyWord && "function".equals(token.text)) {
  // 获取函数名称
  token = this.tokenizer.next();
  // 此处 token 应该为函数名称,在本示例中,应该紧跟左右括号
  if (token.kind == TokenKind.Identifier) {
    Token leftBracketsToken = this.tokenizer.next(); // 左括号
    Token rightBracketsToken = this.tokenizer.next(); // 右括号
    if ("(".equals(leftBracketsToken.text) && ")".equals(rightBracketsToken.text)) {
      // 解析函数体
      FunctionBody body = this.parseFunctionBody();
      return new FunctionDecl(token.text, body);
    } 
  }
}

下一步是实现函数调用的解析,代码如下:

if (token.kind == TokenKind.Identifier) {
  Token leftBracketsToken = this.tokenizer.next(); // 左括号
  if (leftBracketsToken.text.equals("(")) {
    Token nextToken = this.tokenizer.next();
    // 不为 ) 时,里面可能会存在 String 的多个参数,在系统打印函数中定义。
    while (!")".equals(nextToken.text)) {
      if (nextToken.kind == TokenKind.StringLiteral) {
        params.add(nextToken.text);
      } 
      nextToken = this.tokenizer.next();
      if (!")".equals(nextToken.text)) {
        if (",".equals(nextToken.text)) {
          nextToken = this.tokenizer.next();
        }
      }
    }
    // 消耗一下语句末尾的 ; 号
    nextToken = this.tokenizer.next();
    if (";".equals(nextToken.text)) {
      return new FunctionCall(token.text, params);
    }
  }
}

最后是函数体的解析,代码如下:

// 函数体包含在 {} 内
if ("{".equals(token.text)) {
  // 函数体中仅包含函数调用,所以循环一直调用函数调用解析方法,去解析函数调用,只到没有找到函数调用语句为止。
  FunctionCall functionCall = this.parseFunctionCall();
  while (functionCall != null) {
    calls.add(functionCall);
    functionCall = this.parseFunctionCall();
  }
  token = this.tokenizer.next();
  // 判断最后一个 token 是否为 }, 如果不是,说明代码语法错误。
  if (!"}".equals(token.text)) {
    System.err.println("Expecting '}' in FunctionBody, while we got a " + token.text);
  } else {
    return new FunctionBody(calls);
  }
}

0x05 引用消解

在前文中,包含了很多函数定义以及函数调用,那函数调用处调用的函数是否有被定义呢?所以,需要将函数调用与函数定义的树节点建立关联关系,一方面可以判断当前函数调用是否合法,另一方面,也可以加速代码运行时的效率,避免重复查询函数定义。

针对本次示例中的语法数,需要遍历根节点下的方法调用,以及函数体中的方法调用。代码示例如下:

public void visitProg(Prog prog) {
  this.prog = prog;
  // 遍历当前 prog 下面的所有节点
  for (Statement stmt : this.prog.getStmts()) {
    if (stmt instanceof FunctionCall) {
      this.resolveFunctionCall((FunctionCall) stmt);
    } else {
      this.visitFunctionDecl(((FunctionDecl) stmt).getBody());
    }
  }
}

public void visitFunctionDecl(FunctionBody body) {
  // 遍历函数体里面的所有函数调用
  for (FunctionCall functionCall : body.getFunctionCalls()) {
    this.resolveFunctionCall(functionCall);
  }
}

private void resolveFunctionCall(FunctionCall call) {
  FunctionDecl decl = this.findFunctionDecl(call.getName());
  if (decl != null) {
    call.setDefinition(decl);
  } else {
    if (!"println".equals(call.getName())) {  
    	// 如果不是内置函数,直接报错
    }
  }
}

private FunctionDecl findFunctionDecl(String name) {
  // 第一版本,自定义函数中,不包含参数,所以直接匹配名称。
  for (Statement stmt : this.prog.getStmts()) {
    if (stmt instanceof FunctionDecl && ((FunctionDecl) stmt).getName().equals(name)) {
      return (FunctionDecl) stmt;
    }
  }
  return null;
}

0x06

最后的最后,是让解析出来的代码跑起来,怎么跑? 函数调用就去执行对应的函数。那我们定义好的函数怎么运行呢?定义的函数是由 N 个函数调用形成的,如此往复,就跑起来了。代码示例如下:

public void visitProg(Prog prog) {
  for (Statement stmt : prog.getStmts()) {
    if (stmt instanceof FunctionCall) { // 找到函数调用,去执行这个函数调用
      this.runFunction((FunctionCall) stmt);
    }
  }
}

private void visitFunctionBody(FunctionBody body) {
  // 执行函数体中的方法调用
  for (FunctionCall functionCall : body.getFunctionCalls()) {
    this.runFunction(functionCall);
  }
}

private void runFunction(FunctionCall stmt) {
  // 如果是系统打印函数,直接遍历参数进行打印
  if ("println".equals(stmt.getName())) { 
    for (String parameter : stmt.getParameters()) {
      System.out.print(parameter + " ");
    }
    System.out.println();
  } else {
    // 判断当前函数调用是否已经有申明好的函数,如果有,则执行函数体中的函数调用。
    if (stmt.getDefinition() != null) {
      this.visitFunctionBody(stmt.getDefinition().getBody());
    }
  }
}
学习一门编程语言需要一定的时间和耐心,但如果你想要在短时间内快速精通一门编程语言,可以采取以下几个步骤: 1. 确定学习目标和学习路线:在学习任何一门编程语言之前,你需要先了解它的基本特点和适用领域,然后制定一个明确的学习目标和学习路线。可以从一些优秀的学习资源中了解该语言的基础知识和高级用法,并尽可能地多做一些相关的练习。 2. 实践为王:要精通一门编程语言,最关键的是多做实践。通过自己实际动手编写一些小项目,不断地进行实践,你可以更好地理解该语言的语法规则和编程范式,同时也能够更好地应用所学知识来解决实际问题。 3. 学习相关工具和框架:很多编程语言都有各种工具和框架,这些工具和框架可以大大提高编程效率,同时也能够让你更深入地了解该语言的内部机制。因此,学习相关工具和框架也是快速精通一门编程语言的关键。 4. 阅读相关文档和源码:要精通一门编程语言,必须深入了解其内部机制和设计思想。因此,阅读相关文档和源码是必不可少的。通过阅读源码,你可以更好地理解该语言的实现方式和底层原理。 5. 找到合适的学习方法:每个人都有自己的学习方法和习惯,因此找到合适的学习方法也很重要。可以参加一些线上或线下的编程课程或者是加入一些相关的技术社区,与其他学习者互动交流,共同进步。 总的来说,要想在短时间内快速精通一门编程语言,最关键的是多做实践,同时还需要掌握相关的工具和框架,并且要有一个明确的学习目标和学习路线。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值