JavaScript實現ZLOGO: 用語法樹實現多層迴圈
阿新 • • 發佈:2019-01-06
照例先上演示弱效果圖. 演示地址照舊:
程式碼如下:
開始
迴圈4次
迴圈4次
前進50
左轉90度
到此為止
右轉90度
到此為止
結束
如上文JavaScript實現ZLOGO子集: 測試用例末尾所言, 此文用Antlr進行程式碼分析生成語法樹. 再通過語法樹生成p5js繪製程式碼.
Antlr支援兩種程式碼分析方法, Visitor(監聽者)和Visitor(訪問者). SO上的問答Antlr4 Listeners and Visitors - which to implement?大致說明了區別. 基於有限的實踐, 用Visitor方法生成語法樹似乎在實現上更加方便. 尤其相比 Creating a simple parser with ANTLR一文中使用監聽者+棧來構建語法樹.
Antlr生成工具預設不生成Visitor, 新增-visitor引數後可以生成:
java -cp "antlr-4.7-complete.jar:$CLASSPATH" org.antlr.v4.Tool -Dlanguage=JavaScript -visitor 圈3.g4
下面是"定製訪問器.js"中構建語法樹的部分, 看起來比實現前想的簡單. 預設生成的’圈3Visitor’中, visitXX方法實現都是"this.visitChildren(ctx)", 但那樣會把所有的子節點返回值放進陣列, 形成(至少這裡是)多餘的層次:
定製訪問器.prototype.visit程式 = function(上下文) {
語法樹 = {子節點: this.visit(上下文.宣告())};
return 語法樹;
};
定製訪問器.prototype.visit迴圈 = function(上下文) {
return {
型別: '迴圈',
次數: parseInt(上下文.T數().getText()),
子節點: this.visit(上下文.宣告())};
};
定製訪問器.prototype.visit宣告 = function(上下文) {
return this.visit (上下文.getChild(0));
};
定製訪問器.prototype.visit轉向 = function(上下文) {
var 方向 = 上下文.T轉向().getText();
var 角度 = parseInt(上下文.T數().getText()) * (方向 === "左" ? 1 : -1);
return {型別: '轉向', 引數: 角度};
};
定製訪問器.prototype.visit前進 = function(上下文) {
return {型別: '前進', 引數: parseInt(上下文.T數().getText())};
};
上面的原始碼生成語法樹大致如下所示. 實現上還有很多需要改進的, 比如’前進’和’轉向’現在是兩種’型別’, 但應該是一種; 根節點型別不應為空; 等等:
下面是"編譯.js"中基於語法樹生成指令列表的方法, 之後就與之前一樣根據指令列表生成p5js繪製函式(程式碼也不用修改).
function 生成指令序列(節點) {
var 指令序列 = [];
// TODO: 根節點型別不應為空
if (!節點.型別) {
var 宣告節點 = 節點.子節點;
for (var i = 0; i < 宣告節點.length; i++) {
Array.prototype.push.apply(指令序列, 生成指令序列(宣告節點[i]));
}
} else if (節點.型別 == "迴圈") {
var 指令序列 = [];
for (var i = 0; i < 節點.次數; i++) {
Array.prototype.push.apply(指令序列, 生成指令序列({子節點: 節點.子節點}));
}
} // TODO: 修改型別統一為'指令'
else if (節點.型別 == "前進" || 節點.型別 == "轉向") {
return [{名稱: (節點.型別 == "前進" ? 常量_指令名_前進 : 常量_指令名_轉向), 引數: 節點.引數}];
}
return 指令序列;
}
修改相應測試用例, 以及清理不再使用的監聽器程式碼後. 程式碼已從visitor分支(program-in-chinese/quan3)合併到master.