RUST项目实战——LOGO编译(DAY1)

程序设计:更准确地说,本项目是一个使用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
}

然后遍历传入的语法树中所有的字节码,根据字节码采取不同的动作。这就是大体的思路,马上开始具体实现。

Published by endecoder

MY shitting learning experience

Leave a comment