antlr: 实现BrokenScript(二)

134 阅读5分钟

本节我们的目的是实现BrokenScript的部分表达式和语句,并编写对应的解释器功能;

表达式与语句

语句:

在电脑科学的编程中,语句(又称陈述式叙述述句描述式陈述句)是指令式编程语言中最小的独立元素,表达程序要执行的一些动作。多数语句是以高级语言编写成一或多个语句的序列,用于命令电脑执行指定的一系列操作。单一个语句本身也具有内部结构(例如表达式)。

js中的语句

example:

// 简单语句
a = b + 5; // 复制语句;
dosomething(); // 函数调用语句;
return 20; // return语句;
break up; // break 语句
// 复合语句
{let a = 10;...} // block 语句
for...in // for in 语句

表达式:

在大多数编程语言中,语句与表达式互相对比,两者不同之处在于,语句是为了运作它们的副作用而执行;表达式则一定会传回评估后的结果,而且通常不产生副作用。

js 中的表达式

相比于语句,表达式一定有返回值,语句则不一定有;前者关注运算,后者关注逻辑控制和过程(个人理解);

example:

a++;
condition && condition;
(condition ? ifTrue : ifFalse)`
语法文件编写
grammar BrokenScript;

program
  : statements
  ;

statements
  : statement*
  ;

statement
  : block
  | ifStatement
  | forStatement
  | variableStatement
  | expressionStatement=expression
  | ';'
  ;

block
  : '{' statement+ '}'
  ;

ifStatement
  : If parExpression block (Else block)?
  ;

parExpression
  : '(' expression ')'
  ;

forStatement
  : For forExpressionList block;

forExpressionList:
  '(' forInit=variableStatement? ';' forControl=expression? ';' forUpdate=expression? ')' // 起个别名区分这几个表达式
  ;

variableStatement
  : VarType variableDeclareList 
  ;

variableDeclareList
  : variableDeclare (',' variableDeclare)*
  ;
variableDeclare
  : Identifier ('='expression)?
  ;

expression
  : primary
  | expression bop=('*'|'/') expression
  | expression bop=('+'|'-') expression
  | expression bop=('>'|'>='|'<'|'<=') expression
  | expression bop=('='|'+='|'-=') expression
  ;

primary
  : '(' expression ')'
  | IntLiteral
  | Identifier
  ;
  
IntLiteral
  : '0' | [1-9][0-9]*
  ;

限于篇幅,这里只列出了语法部分,词法部分看github代码链接即可(词法规则首字母大写);

我们写语法文件也就是描述我们的程序构成;比如上述代码我们先定义program : statements; 指定脚本程序是由多个语句构成,然后定义语句statement: block|ifStatement|forStatement...; 指定语句集合里有块语句,if语句,for语句等,这样我们依次从上到下依次推导,直到我们的规则不能再向下分解;比如我们推导到了primary: IntLiteral | Identifier, IntLiteral由数字构成,不可能再分解了,所以我们的语法描述也就完成了;

优先级

需要注意的是,规则是有顺序的,先写的规则先匹配;上面的表达式,先匹配primary基础表达式,然后匹配乘除运算,其次加减运算,其次比较远算,再其次赋值运算;

比较笨的办法,伪代码如下:

let matchStart;
// 例如先加减后乘除
const matchExpression = () => {
    // tokenList开始匹配的标志位;
    const start = matchStart;
    const node = matchAddTypes(); // match 中会改变matchStart;
    // match上了
    if (node) {
        // 继续看后面
    }
    // 重置标志位;回溯
    matchStart = start;
    const node = matchCompareTypes();
    if (node) {
        // 继续看后面
    }
    matchStart = start;
    //...
}

因为我们在每次递归的匹配中都是先匹配的优先级最高的,所以优先级高的也会在树的底部;这种我们计算的时候比较明显看出:例如计算 3 + 5 * (12-7);我们进行递归求值的时候调用栈中肯定是12-7的计算函数先返回结果(在最上方),然后一层层向外层退;

image.png

解释器实现

在实现逻辑控制语句和循环语句之前,我们先实现表达式和变量声明,然后打印脚本最后执行的返回值

新建variable.b文件,内容如下:

let a = 20, c = 25;
a = 12 + c;

image.png

我们要为路径上的每一个节点写visit函数,可以看成这样,我们要获取program的返回值,我们就要获取statements最后一个语句的返回值;获取statement的返回值,就要获取variable Statement的返回值,依次到求得的expression的值;

antlr 的 BrokenScriptVisitor.js 里面为每一个规则都创建了一个visitRule的方法,入参即是ast节点(里面可以通过ctx[childRule]获取子节点的ast node)

我们写的时候对照这语法文件从上往下写就好;

export default class Evaluate extends BrokenScriptVisitor {
 visitProgram(ctx) {
   return this.visitStatements(ctx.statements());
 }
 visitStatements(ctx) {
   let returnValue = null;
   for (let child of ctx.statement()) {
     if (child.getText() === ";") {
       continue;
     }
     returnValue = this.visitStatement(child);
   }
   // 返回最后一个语句的返回值
   return returnValue;
 }
 visitStatement(ctx) {
   let returnValue = null;
   if (ctx.ifStatement()) {
     returnValue = this.visitIfStatement(ctx.ifStatement());
   } else if (ctx.forStatement()) {
     returnValue = this.visitForStatement(ctx.forStatement());
   } else if (ctx.variableStatement()) {
     returnValue = this.visitVariableStatement(ctx.variableStatement());
   } else if (ctx.expression()) {
     returnValue = this.visitExpression(ctx.expression());
   }
   return returnValue;
 }
 ...
}

因为引入了变量,所以需要新增一个map来存储变量的值,我们在访问variableDeclare的时候将变量名加入map,如果给变量了初始值,则设置为初始值,否则设置为undefined;

visitVariableDeclare(ctx) {
    let idName = ctx.Identifier().getText();
    let value = undefined;
    if (ctx.expression()) {
      // Identifier '=' expression
      value = this.visitExpression(ctx.expression());
    }
    this.variableContainer.set(idName, value);
    return value;
}

在使用的时候我们判断下是否已经声明了该变量,如果没有的话则报错:

// 到这是表达式使用了,看看是否在前面定义了
visitPrimary(ctx) {
    let value = null;
    if (ctx.IntLiteral()) {
      value = Number(ctx.IntLiteral().getText());
    } else if (ctx.Identifier()) {
      const idName = ctx.Identifier().getText();
      if (this.variableContainer.has(idName)) {
        value = this.variableContainer.get(idName);
      } else {
        // 可以添加更详细的信息,例如第几行第几列
        throw new Error(`变量未定义:${idName}`);
      }
    }
    return value;
}

表达式实现部分:

visitExpression(ctx) {
    let value = null;
    // 现在 express的规则 primary | expression opt expression 所以只有长度是3和1两种可能
    if (ctx.children.length === 3) {
      let bopType = ctx.bop.type;
      let left = ctx.expression(0);
      let right = ctx.expression(1);

      switch (bopType) {
        case BrokenScriptParser.ADD:
          value = this.add(left, right);
          break;
        case BrokenScriptParser.SUB:
          value = this.sub(left, right);
          break;
        case BrokenScriptParser.ASSIGN:
          let rightValue = this.getValueByCtx(right);
          if (!left.primary()?.Identifier()) {
            throw new Error(
              `语法错误,不能将${right.getText()}赋值给${left.getText()}`
            );
          } else {
            const idName = left.primary().Identifier().getText();
            this.variableContainer.set(idName, rightValue);
            value = rightValue;
          }
          break;
        case BrokenScriptParser.GT:
          value = this.gt(left, right);
          break;
        case BrokenScriptParser.LT:
          value = this.lt(left, right);
          break;
        default:
          console.warn("未实现的操作类型");
      }
    } else if (ctx.primary()) {
      value = this.visitPrimary(ctx.primary());
    }
    return value;
}

add(left, right) {
    return this.getValueByCtx(left) + this.getValueByCtx(right);
}

getValueByCtx(ctx) {
    if (ctx.children.length === 3) {
      // a + b 15 >17 这种求值还是接着调用visitExpression
      return this.visitExpression(ctx);
    } else {
        // 看上面的visitPrimary ‘15’返回15, a 返回a的值
      return this.visitPrimary(ctx.primary());
    }
}

至此,我们实现了表达式求值和赋值语句,来验证下成果吧: 修改一下variable.c

let a = 20, c = 25;
let c = 50;
a = 12 + c;

添加index.js

import fs from "fs";
import compile from "./src/compile.js";

const input = fs.readFileSync("./scripts/variable.b", {
  encoding: "utf-8",
});

const result = compile(input);

console.log({ result });

image.png

输出正确,interesting!

下面实现循环和控制语句就简单些了;

控制语句,根据执行的conditionValue来选择执行里面的block;

visitIfStatement(ctx) {
    let returnValue;
    const conditionValue = this.visitParExpression(ctx.parExpression());
    if (conditionValue) {
      returnValue = this.visitBlock(ctx.block(0));
    } else if (ctx.Else()) {
      returnValue = this.visitBlock(ctx.block(1));
    }
    return returnValue;
}

循环语句,根据执行的forControl来重复执行里面的block;

visitForStatement(ctx) {
    const forExpressionList = ctx.forExpressionList();
    const block = ctx.block();
    const forInit = forExpressionList.forInit;
    const forUpdate = forExpressionList.forUpdate;
    const forControl = forExpressionList.forControl;
    let returnValue = null;

    if (forInit) {
      this.visitVariableStatement(forInit);
    }

    while (true) {
      const condition = forControl ? this.visitExpression(forControl) : true;
      if (!condition) {
        break;
      }
      returnValue = this.visitBlock(block);
      if (forUpdate) {
        this.visitExpression(forUpdate);
      }
    }

    return returnValue;
}

我们选择if.b 和 loop.b执行,输出也是正确的; if.b

let a = 20;

if (a > 25) {
  a = a + 20;
} else {
  a = a - 10;
}
a; // 10

loop.b

let a = 0;
for (let i = 0; i < 0; i=i+1) {
  a = a + 2;
}
a; // 0

现在我们的解释器还是有点问题,例如判断等号左边是否是变量的判断不太优雅,没有块作用域,执行的时候才能知道变量是否定义等问题,我们下面会继续迭代,下一节我们会添加块作用域;

代码地址:github.com/YanZiSen/br…

参考链接:编译原理之美

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情