菜单
本页目录

18b-REPL 命令解析 - 你想构建一个语言虚拟机吗?

引言

你好!在這一部分中,我们将要在 REPL 中去解析用户输入的命令。目前,还不够灵活;用戶不能输入这样的:

.load_file /path/to/file

目前,他们必须像这样才行:

.load_file\r
Please enter file path: /path/to/file\r

命令等等 (Commands and Such)

首先,我们需要做一些小改动。你可能已经注意到,我们在汇编语言中使用了 . 字符,所以我们需要使用其他字符。目前,我们将使用 ! 来代替。

让我们将这个功能作为 repl 模块的一个子模块。你可以创建 src/repl/command_parser

步骤 1:命令解析器结构

这并不复杂。我们只需要一个函数来对输入进行标记化:

pub struct CommandParser {}

impl CommandParser {
    pub fn tokenize(input: &str) -> Vec<&str> {
        let split = input.split_whitespace();
        let vec: Vec<&str> = split.collect();
        vec
    }
}

这里没有必要创建这个结构体的实例。我们只需要按空格 (whitespace) 分割用户输入的字符串,并返回这些标记。

别忘了在 src/repl/mod.rs 中添加 pub mod command_parser;

步骤 2:拆分函数

你知道我们有一个大的匹配块来检查 .whatever 吗?每一个选项都应该是它自己的函数,像这样:

fn quit(&mut self, args: &[&str]) {
    println!("Farewell! Have a great day!");
    std::process::exit(0);
}

我不会在本教程中展示每一个函数,因为它只是简单的复制粘贴。

步骤 3:运行函数更改

更有趣的是我们必须如何改变解析逻辑。这是 src/repl/mod.rs 中新的 run 函数:

pub fn run(&mut self) {
    println!("Welcome to Iridium! Let's be productive!");
    loop {
        // 这会在每次循环中分配一个新的字符串,用来存储用户输入的内容。
        // TODO: 想办法在循环外分配这个,并在每次循环中重复使用它
        let mut buffer = String::new();

        // 阻塞调用,直到用户输入命令
        let stdin = io::stdin();

        // 令人烦恼的是,`print!` 不像 `println!` 那样自动刷新 stdout,所以我们
        // 必须在那里做,以便用户能看到我们的 `>>>` 提示。
        print!(">>> ");
        io::stdout().flush().expect("Unable to flush stdout");

        // 在这里我们将查看用户给我们的字符串。
        stdin
            .read_line(&mut buffer)
            .expect("Unable to read line from user");

        let historical_copy = buffer.clone();
        self.command_buffer.push(historical_copy);

        if buffer.starts_with("!") {
            self.execute_command(&buffer);
        } else {
            let program = match program(CompleteStr(&buffer)) {
                Ok((_remainder, program)) => {
                    program
                },
                Err(e) => {
                    println!("Unable to parse input: {:?}", e);
                    continue;
                }
            };
            self.vm.program.append(&mut program.to_bytes(&self.asm.symbols));
            self.vm.run_once();
        }
    }
}

这里是新的 execute_command 函数:

fn execute_command(&mut self, input: &str) {
    let args = CommandParser::tokenize(input);
    match args[0] {
        "!quit" => self.quit(&args[1..]),
        "!history" => self.history(&args[1..]),
        "!program" => self.program(&args[1..]),
        "!clear_program" => self.clear_program(&args[1..]),
        "!clear_registers" => self.clear_registers(&args[1..]),
        "!registers" => self.registers(&args[1..]),
        "!symbols" => self.symbols(&args[1..]),
        "!load_file" => self.load_file(&args[1..]),
        "!spawn" => self.spawn(&args[1..]),
        _ => { println!("Invalid command") }
    };
}

注意我们如何从传递给每个独立函数的切片中剥离出命令的部分,这样它们就得到了参数而不是带有!命令的副本。

UTF-8 烦恼 (Annoyances)

因为 Rust 中的所有字符串都是 UTF-8 编码,你不能像期望的那样用 &buffer[0] 来检查第一个字符。我曾对如何不制作大量副本就检查第一个字符是否为 "!" 感到烦恼。

幸运的是,我发现了 starts_with 函数!似乎有很多有用的便利函数潜伏着。

结束

本文到此结束!这些更改将包含在 0.0.18 版本中。我正在努力找出一种好的方法来同步版本与教程。代码可以在 GitLab 上找到!

原文出处:REPL Command Parsing - So You Want to Build a Language VM 作者名:Fletcher