本节我们的目的是实现BrokenScript的部分表达式和语句,并编写对应的解释器功能;
表达式与语句
语句:
在电脑科学的编程中,语句(又称陈述式、叙述、述句、描述式、陈述句)是指令式编程语言中最小的独立元素,表达程序要执行的一些动作。多数语句是以高级语言编写成一或多个语句的序列,用于命令电脑执行指定的一系列操作。单一个语句本身也具有内部结构(例如表达式)。
example:
// 简单语句
a = b + 5; // 复制语句;
dosomething(); // 函数调用语句;
return 20; // return语句;
break up; // break 语句
// 复合语句
{let a = 10;...} // block 语句
for...in // for in 语句
表达式:
在大多数编程语言中,语句与表达式互相对比,两者不同之处在于,语句是为了运作它们的副作用而执行;表达式则一定会传回评估后的结果,而且通常不产生副作用。
相比于语句,表达式一定有返回值,语句则不一定有;前者关注运算,后者关注逻辑控制和过程(个人理解);
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的计算函数先返回结果(在最上方),然后一层层向外层退;
解释器实现
在实现逻辑控制语句和循环语句之前,我们先实现表达式和变量声明,然后打印脚本最后执行的返回值
新建variable.b文件,内容如下:
let a = 20, c = 25;
a = 12 + c;
我们要为路径上的每一个节点写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 });
输出正确,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
现在我们的解释器还是有点问题,例如判断等号左边是否是变量的判断不太优雅,没有块作用域,执行的时候才能知道变量是否定义等问题,我们下面会继续迭代,下一节我们会添加块作用域;
参考链接:编译原理之美
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情