一直對技術有很強的興趣,終於,決定要寫自己的語言(m語言)。那就先從最簡單的開始:解釋執行器。
一套完整的語言包含的肯定不止解釋執行器了,還要有編譯器和IDE,也就還要有語法高亮、智能提示等,不過還沒學會那些,先搞個最基本的解釋執行器。
思路如下:
- 定義好希望的語法(基本語句有:順序執行、if語句、for語句、while語句、系統自有函數定義、用戶函數定義、函數調用)
- 找一款詞法語法解析器工具,讓字符串流變成語法書(AST)
- 編寫解釋執行器
- 元數據收集
- 變量作用域定義、查找
- 解釋執行
先設想我們的m語言語法要怎么牛b啊,比如下面這段demo語法代碼:
go 計算標准體重(年齡) { 體重:年齡*3; 體重; } 體重:10; a:10; a:輸出(體重); b:25; a:100+10+b; 輸出(a); (a==135)-> { a:a+a+a; 輸出(a); } else { 輸出(b); }; a:1; while a<10 ->{ a:a+2; 輸出(a); }; 輸出("WHILE OK"); repeat i from 0 to 100 step 10->{ 輸出(i); } init->{ 輸出("FOR INIT"); } onerror->{ 輸出("FOR ERROR"); } finally->{ 輸出("FOR FINALLY"); }; 輸出('FOR OK'); a:10; 輸出(計算標准體重(a));
很顯然,第一個語句塊是用戶函數的定義方式,以"go"字符串為函數定義的開始,接着是常規的函數名稱、參數、函數方法塊。
剩下的大致上就是順序執行了,其中穿插着一些循環語句等,repeat循環自定義的比較厲害,好叼。。。感覺。。真的好叼。。。。
每個語句以封號后綴結束、賦值以冒號來標識。
接着來看看基於ANTLR的詞法定義:
m.g4:
grammar m;
import basic,function,assignStmt,ifStmt,forStmt,whileStmt;
nomalStmt
:assignStmt
|ifStmt
|forStmt
|whileStmt
;
declarationStmt
:functionDeclare
;
stmt
:nomalStmt LS
|declarationStmt
;
program
: stmt+
;
由於詞法語法定義較多,不貼代碼了,可以下載代碼看全部的(基於ideas/需要安裝antlr4插件)
接下來是時候讓我們load進demo代碼解析成AST樹啦:
String code=Utils.readTxtFile("F:\\BaiduYunDownload\\mLanguage(4)\\m_code2.m");//這個是放demo代碼的文件 code=code.substring(1);//去掉第一個特殊字符 CharStream is = CharStreams.fromString(code); //antlr對象,讀入字符串 mLexer lexer = new mLexer(is); //mLexer是antlr自動生成的一個詞法類 CommonTokenStream tokens = new CommonTokenStream(lexer); //antlr對象 mParser parser = new mParser(tokens); //mParser是antlr自動生成的一個此法解析類 mParser.ProgramContext tree=parser.program(); //program是入口規則,根規則 program program= NodeParser.parseProgram(tree); //自己寫的NodeParser類,需要一堆自定義的節點類型配合解析整棵AST樹 mRuntime runtime=new mRuntime(program); runtime.plainInterpreter(); //解釋器執行 System.out.println("");
AST節點的定義:
demo代碼構建成AST樹的效果圖(antlr插件中能看):
轉換成為AST樹后,剩下的就是編寫解釋執行器,其實相當於前端編譯器。
主要步驟是3步:
- 收集元數據
- 定義變量作用域
- 語句塊的解釋執行
public void execute(program program) { //1. 先掃描函數定義,收集元數據 collectMetaData(program); //2. 變量作用域 walkAST4Variables(program); //3. 解釋執行代碼 runCode(program); }
1. 收集元數據,其實就是對自定義函數的收集,統一放到一個Dictionary里,以便到時候引用到了執行語句塊(和參數的傳遞)
private void collectMetaData(program program) { for(com.mckay.language.m.core.nodes.m.stmt stmt:program.stmts) if(stmt.declarationStmt!=null) this.userDefinedFunctionSymbols.defineMethod(stmt.declarationStmt.functionDeclare.functionIdentifier.getIdentifier(), stmt.declarationStmt.functionDeclare); } public class UserDefinedFunctionSymbols { private Dictionary<String, functionDeclare> methods=new Hashtable<>(); public functionDeclare getMethod(String identifier) { return methods.get(identifier); } public void defineMethod(String identifier, functionDeclare ast) { methods.put(identifier, ast); } }
functionDeclare是具體的node,屬於AST中眾多節點類型中的一種,代表函數聲明節點。
2. 定義變量作用域,由於存在函數(自定義函數、系統自帶函數),因此需要有變量Scope的概念,存在局部變量覆蓋全局變量現象
private void walkAST4Variables(program program) { program.VariableSymbols=globalVariableSymbol; for(com.mckay.language.m.core.nodes.m.stmt stmt:program.stmts) { stmt.VariableSymbols=program.VariableSymbols; if(stmt.declarationStmt!=null) { stmt.declarationStmt.VariableSymbols=stmt.VariableSymbols; VarWalker.walk(stmt.declarationStmt); } if(stmt.nomalStmt!=null) { stmt.nomalStmt.VariableSymbols=stmt.VariableSymbols; VarWalker.walk(stmt.nomalStmt); } } } public class VariableSymbol { private Dictionary<String, Variable> variables=new Hashtable<>(); private VariableSymbol parentVariableSymbol; public void setParentVariableSymbol(VariableSymbol parentVariableSymbol) { this.parentVariableSymbol=parentVariableSymbol; } public void defineVariable(String name, Variable variable) { variables.put(name, variable); } public void setValue(String name, Object value) { Variable variable=getVariable(name); variable.Value=value; } public Object getValue(String name) { Variable variable=getVariable(name); return variable.Value; } private Variable getVariable(String name) { List<String> keys=Collections.list(variables.keys()); if(keys.contains(name)) return this.variables.get(name); if(this.parentVariableSymbol!=null) return this.parentVariableSymbol.getVariable(name); throw new RuntimeException("變量未定義"); } }
當局部變量中沒有找到本地變量定義時,會根據parent關聯向上找變量,直到為null。
3. 語句塊的解釋執行,這個可以說是最容易理解的地方了
private void runCode(program program) { StmtExecutor executor=new StmtExecutor(this); for(com.mckay.language.m.core.nodes.m.stmt stmt:program.stmts) if(stmt.nomalStmt!=null) executor.execute(stmt.nomalStmt); }
StmtExecutor.execute(nomalStmt)會調用一系列子語句,如下圖就一圖就懂:
如上圖中,針對expression是調用calc的,一堆calc,expression中套expression。
system built-in函數的定義,是通過NativeMethodNode.setCode來標識的,比如當前實現的code為OUTPUT,功能如下:System.out.print/Console.Write()
第一個紅框是native node中判斷code是哪個system built-in函數的編碼代號
第二個紅框是對應built-in函數的java語句執行。
demo m代碼對應的解釋執行輸出:
10 135 405 3 5 7 9 11 WHILE OK FOR INIT 0 10 20 30 40 50 60 70 80 90 100 FOR FINALLY FOR OK 30 ok
代碼下載(基於java)