程序设计:更准确地说,本项目是一个使用RUST语言编写的LOGO语言解释器而非编译器。所以以后的博客名称会更名为LOGO解释器。(大爷我可是相当严谨的)
设计参考:《使用Rust实现LUA解释器》
设计参考网址:https://wubingzheng.github.io/build-lua-in-rust/zh/PREFACE.html
参考作者github页面:https://github.com/WuBingzheng
解释器的主要任务是将源LOGO代码转换为可执行的Rust代码,原LOGO代码主要有以下部分组成:
MAKE "DISTANCE "3
/////////////////////////////
指令(string) -> 参数名(string) -> 参数值(number)
/////////////////////////////
PENUP
/////////////////////////////
指令(string)
先介绍两个基本概念:“字节码(bytecode)”与“值(value)”
字节码:
- 通常指的是已经经过编译,但与特定机器代码无关,需要解释器转译后才能成为机器代码的中间代码。字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令等构成的序列。(个人认为在这里RUST语言就承担了字节码的作用,是承接源码(LOGO)与可执行的机器码(二进制文件)的一种中间代码)
值:
- 个人认为就是变量,很幸运的是在我们的程序中值只可能是数字或者是空值,任何字符类型的传入都会被定义为错误
直接开始项目,将项目分为几个大的部分:
- 程序入口
- 词法分析,语法分析,虚拟机
- 字节码与值
程序入口已在start code中给出,具体代码和解释如下:
use clap::Parser;
use unsvg::Image;
#[derive(Parser)]
struct Args {
/// 原LOGO代码路径
file_path: std::path::PathBuf,
/// 生成图片路径
image_path: std::path::PathBuf,
/// Height
height: u32,
/// Width
width: u32,
}
fn main() -> Result<(), ()> {
let args: Args = Args::parse();
//由空格分隔读取命令行数据
let file_path = args.file_path;
let image_path = args.image_path;
let height = args.height;
let width = args.width;
let image = Image::new(width, height);
match image_path.extension().map(|s| s.to_str()).flatten() {
Some("svg") => {
let res = image.save_svg(&image_path);
if let Err(e) = res {
eprintln!("Error saving svg: {e}");
return Err(());
}
}
Some("png") => {
let res = image.save_png(&image_path);
if let Err(e) = res {
eprintln!("Error saving png: {e}");
return Err(());
}
}
_ => {
eprintln!("File extension not supported");
return Err(());
}
}
Ok(())
}
这里将源文件路径,生成图片的路径,图片的宽与高读取在一个struct中,后面两大段主要作用于生成图片的保存,并在保存不成功时抛出错误。这里能看到,该代码并没有包含文件的读取,需要我自己写(草泥马)。
先以一个非常简单的LOGO程序开始,然后再加上烦人的细节,LOGO代码如下:
PENDOWN
FORWARD "50
BACK "80
////////////////////////
落笔,往前50码,往后80码
可以构建一个stack结构来保存每一个token(命令、数值、变量名均为token),stack结构可以用下图表示:
-----------
| FORWARD | <- token 1 (命令)
-----------
| "50 | <- token 2(数值)
-----------
| | <- token 3
-----------
| |
-----------
·首先把名为FORWARD的全局变量加载到栈(0)位置;
·然后把字符串常量(以新的struct表示)"50加载到栈(1)位置;
·然后执行栈(0)位置的函数,并把栈(1)位置作为参数。
我想到了一个简单的方法表示token:将整行语句读取为字符串,然后按照空格划分存入stack之中,在该简单示例中语法比较简单,可以根据读取的token数量将命令划分为不同的种类:
- 一个token:PENUP, PENDOWN
- 两个token:FORWARD, BACK
事实上我也是这么做的。为了说明进一步的设计,这里要再对字节码做一个分析。上文已经给出了字节码的定义,即一个转化源码与机器码的中介,虽说这里可以直接使用RUST作为字节码,可我才疏学浅还没能想到怎么直接转化,不妨自己重新定义一个字节码。以原LOGO文件中这一句为例:
FORWARD "50
要执行这一句,我们需要做如下的事情在上面的图示中表达的很清楚,其涉及三个不同的字节码:
- GetGlobal(i32,i32): 获得全局变量名(在stack中的位置,全局变量在常量表中的位置)
- Loadconst(i32,i32):加载常量(在stack中的位置,常量在常量表中的位置)
- Call(i32,i32):调用函数(调用的函数在stack中的位置,参数在stack中的位置)
代码的执行逻辑就是:
- 读取LOGO指令
- 将其转化为语法树结构(ParseProto):包含常量表与字节码
- 将生成的语法树转入虚拟机中执行
其中,语法树的定义如下:
pub fn load(input: File) -> ParseProto {
let mut constants = Vec::new(); //常量表
let mut byte_codes = Vec::new(); //字节码
let mut lex = Lex::new(input); //Lex为转入的词法结构
} //简化版代码,不一定准确
虚拟机需要做的就是,构建一个HashMap,将经过整理的语法树所传入的命令函数(command)与Rust代码中执行相同任务的命令函数对应起来。可以构建如下结构来清晰表明结果:
pub struct ExeState {
globals: HashMap<String, Value>, //全局变量对应表
stack: Vec::<Value>, //这就是栈 -> stack
}
然后遍历传入的语法树中所有的字节码,根据字节码采取不同的动作。这就是大体的思路,马上开始具体实现。
