Add macro evaluator with %include implementation

This commit is contained in:
2024-06-03 10:41:11 -07:00
parent 4730f11f31
commit 0ac9ee4a3f
5 changed files with 189 additions and 2 deletions
+70
View File
@@ -0,0 +1,70 @@
use anyhow::{anyhow, Result};
use codepage_437::CP437_WINGDINGS;
// Serialize multi-line content with CR-terminated lines
fn encode_multiline(input: &str) -> Result<Vec<u8>> {
input
.chars()
.map(|c| {
if c == '\n' {
Ok('\r' as u8)
} else {
CP437_WINGDINGS
.encode(c)
.ok_or(anyhow!("Couldn't encode char: {}", c))
}
})
.collect()
}
fn decode_multiline(input: &[u8]) -> String {
input
.iter()
.map(|&x| {
if x == '\r' as u8 {
'\n'
} else {
CP437_WINGDINGS.decode(x)
}
})
.collect()
}
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;
use crate::encoding::{decode_multiline, encode_multiline};
fn serialize_code(input: &str) -> Vec<u8> {
encode_multiline(input).expect("Error in test")
}
#[test]
fn roundtrip_multiline() {
let bytes: Vec<u8> = (0..=255).collect();
assert_eq!(bytes, serialize_code(&decode_multiline(&bytes)))
}
#[test]
fn newlines() {
assert_debug_snapshot!(serialize_code("ABC\nDEF"), @r###"
[
65,
66,
67,
13,
68,
69,
70,
]
"###)
}
#[test]
fn wingdings() {
assert_debug_snapshot!(decode_multiline(&(0..32).collect::<Vec<_>>()), @r###""\0☺☻♥♦♣♠•◘○◙♂♀\n♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼""###);
assert_debug_snapshot!(decode_multiline(&(112..144).collect::<Vec<_>>()), @r###""pqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅ""###);
assert_debug_snapshot!(decode_multiline(&(224..=255).collect::<Vec<_>>()), @r###""αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}""###);
}
}
+1
View File
@@ -1,3 +1,4 @@
mod encoding;
mod preprocess;
mod world;
+115
View File
@@ -0,0 +1,115 @@
use std::{fs, path::Path};
use anyhow::{anyhow, bail, Result};
use super::{
parse::{parse, Expr},
scan::scan,
};
pub struct Context {
file_loader: Box<dyn FileLoaderTrait>,
}
trait FileLoaderTrait {
fn load(&self, path: &Path) -> Result<String>;
}
struct FileLoader;
struct MockFileLoader {
content: String,
}
impl FileLoaderTrait for FileLoader {
fn load(&self, path: &Path) -> Result<String> {
fs::read_to_string(path).map_err(|e| anyhow!("Couldn't load file: {}", e))
}
}
impl FileLoaderTrait for MockFileLoader {
fn load(&self, _path: &Path) -> Result<String> {
Ok(self.content.clone())
}
}
impl Context {
pub fn eval_program(&self, input: &str) -> Result<String> {
let tokens = scan(input).0;
let exprs = parse(tokens)?;
let mut result: Vec<String> = vec![];
for expr in exprs {
match expr {
Expr::ZztOop(s) => result.push(s),
Expr::Macro(name, args) => match name.as_str() {
"include" => {
if args.len() != 1 {
bail!("wrong number of args for %include");
}
let filename = if let Expr::String(s) = args[0].as_ref() {
s
} else {
bail!("%include filename must be a string")
};
let mut content = self.file_loader.load(Path::new(filename))?;
content = content.replace("\r\n", "\n");
if content.ends_with("\n") {
content.pop();
}
result.push(content)
}
_ => bail!("Unknown macro: {:?}", name),
},
_ => {
bail!("Unexpected expr: {:?}", expr);
}
}
}
Ok(result.join(""))
}
}
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;
use super::*;
fn make_context(data: String) -> Context {
Context {
file_loader: Box::new(MockFileLoader { content: data }),
}
}
#[test]
fn include() {
let program = format!("foo\n%include \"bb.txt\"\nquux");
let file = "bar\nbaz\n";
assert_debug_snapshot!(make_context(file.into()).eval_program(&program), @r###"
Ok(
"foo\nbar\nbaz\nquux",
)
"###);
}
#[test]
fn include_windows() {
let program = format!("%include \"foo.txt\"");
let file = "foo\r\nbar";
assert_debug_snapshot!(make_context(file.into()).eval_program(&program), @r###"
Ok(
"foo\nbar",
)
"###);
}
#[test]
fn unknown_macro() {
make_context("".into())
.eval_program("%foo")
.expect_err("Expected error: unknown macro");
assert_debug_snapshot!(make_context("".into())
.eval_program("%foo")
.expect_err("Expected error: unknown macro"), @r###""Unknown macro: \"foo\"""###);
}
}
+1
View File
@@ -1,2 +1,3 @@
pub mod eval;
pub mod parse;
pub mod scan;
+2 -2
View File
@@ -76,7 +76,7 @@ impl Parser {
args.push(Box::new(Expr::String(val)));
}
Token::Newline => {
self.advance();
//self.advance();
break;
}
_ => return Err(anyhow!("Unexpected token {:?}", token)),
@@ -163,7 +163,7 @@ mod tests {
[],
),
ZztOop(
"Baz.",
"\nBaz.",
),
]
"###)