Tests for build_ast

main
Shad Amethyst 9 months ago
parent b89b3b9470
commit 987c5f63b0

@ -9,6 +9,24 @@ pub enum BasicAstExpression {
Binary(Operator, Box<BasicAstExpression>, Box<BasicAstExpression>),
}
macro_rules! impl_op_basic_ast_expression {
( $std_op:ty, $fn_name:ident, $self_op:expr ) => {
impl $std_op for BasicAstExpression {
type Output = BasicAstExpression;
fn $fn_name(self, other: Self) -> Self {
Self::Binary($self_op, Box::new(self), Box::new(other))
}
}
};
}
// These are primarily here for ease of use in testing
impl_op_basic_ast_expression!(std::ops::Add, add, Operator::Add);
impl_op_basic_ast_expression!(std::ops::Sub, sub, Operator::Sub);
impl_op_basic_ast_expression!(std::ops::Mul, mul, Operator::Mul);
impl_op_basic_ast_expression!(std::ops::Div, div, Operator::Div);
#[derive(Clone, Debug, PartialEq)]
pub enum BasicAstOperation {
Assign(String, BasicAstExpression),
@ -22,11 +40,28 @@ pub struct BasicAstInstruction {
pub operation: BasicAstOperation,
}
impl From<BasicAstOperation> for BasicAstInstruction {
fn from(operation: BasicAstOperation) -> Self {
Self {
label: None,
operation,
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct BasicAstBlock {
pub instructions: Vec<BasicAstInstruction>,
}
impl BasicAstBlock {
pub fn new(instructions: impl IntoIterator<Item = BasicAstInstruction>) -> Self {
Self {
instructions: instructions.into_iter().collect(),
}
}
}
/// Returns the index of the first token matching `needle`
fn find_token_index(tokens: &[BasicToken], needle: BasicToken) -> Result<usize, ParseError> {
tokens
@ -122,13 +157,13 @@ pub fn build_ast(tokens: &[BasicToken]) -> Result<BasicAstBlock, ParseError> {
let mut current_label: Option<String> = None;
while tokens.len() > 0 {
match tokens.peek(2) {
match tokens.peek(3) {
[BasicToken::NewLine, BasicToken::Integer(label), ..] => {
tokens.take(2);
current_label = Some(label.to_string());
}
[BasicToken::NewLine, BasicToken::Name(label), ..] => {
tokens.take(2);
[BasicToken::NewLine, BasicToken::Name(label), BasicToken::LabelEnd, ..] => {
tokens.take(3);
current_label = Some(label.clone());
}
[BasicToken::NewLine, ..] => {
@ -173,9 +208,28 @@ pub fn build_ast(tokens: &[BasicToken]) -> Result<BasicAstBlock, ParseError> {
});
}
tokens.take(end_index);
tokens.take(end_index + 1);
}
[BasicToken::Goto, BasicToken::Integer(label), ..] => {
tokens.take(2);
instructions.push(BasicAstInstruction {
label: current_label.take(),
operation: BasicAstOperation::Jump(label.to_string()),
});
}
[BasicToken::Goto, BasicToken::Name(label), ..] => {
tokens.take(2);
instructions.push(BasicAstInstruction {
label: current_label.take(),
operation: BasicAstOperation::Jump(label.clone()),
});
}
_ => {
if cfg!(test) {
eprintln!("No match found in main ast loop!");
eprintln!("Lookahead: {:?}", tokens.peek(5));
eprintln!("Instructions: {:?}", instructions);
}
return Err(ParseError::UnexpectedToken(tokens[0].clone()));
}
}

@ -235,3 +235,102 @@ fn test_operator_precedence() {
ParseError::UnexpectedToken(BasicToken::Operator(Operator::Add))
);
}
fn test_build_ast(raw: &str) -> BasicAstBlock {
let tokens = tokenize(raw).unwrap_or_else(|e| {
panic!(
"Error while tokenizing: {:?}\nProgram:\n```\n{}\n```",
e, raw
);
});
let parsed = build_ast(&tokens).unwrap_or_else(|e| {
panic!(
"Error while parsing: {:?}\nProgram:\n```\n{}\n```\nTokens:\n{:#?}",
e, raw, tokens
);
});
parsed
}
#[test]
fn test_ast_basics() {
assert_eq!(
test_build_ast("LET X = 2 * 5\n"),
BasicAstBlock::new([BasicAstOperation::Assign(
String::from("X"),
BasicAstExpression::Integer(2) * BasicAstExpression::Integer(5)
)
.into()])
);
assert_eq!(
test_build_ast("IF X < 0 THEN\nX = 0-X\nEND IF"),
BasicAstBlock::new([BasicAstOperation::IfThenElse(
BasicAstExpression::Binary(
Operator::Lt,
Box::new(BasicAstExpression::Variable(String::from("X"))),
Box::new(BasicAstExpression::Integer(0))
),
BasicAstBlock::new([BasicAstOperation::Assign(
String::from("X"),
BasicAstExpression::Integer(0) - BasicAstExpression::Variable(String::from("X"))
)
.into()]),
BasicAstBlock::default()
)
.into(),])
);
assert_eq!(
test_build_ast("GOTO 10\nGOTO hello"),
BasicAstBlock::new([
BasicAstOperation::Jump(String::from("10")).into(),
BasicAstOperation::Jump(String::from("hello")).into(),
])
);
}
#[test]
fn test_ast_labels() {
assert_eq!(
test_build_ast("10 LET X = 0\n20 LET Y = 2 * X\n"),
BasicAstBlock::new([
BasicAstInstruction {
label: Some(String::from("10")),
operation: BasicAstOperation::Assign(
String::from("X"),
BasicAstExpression::Integer(0)
)
},
BasicAstInstruction {
label: Some(String::from("20")),
operation: BasicAstOperation::Assign(
String::from("Y"),
BasicAstExpression::Integer(2)
* BasicAstExpression::Variable(String::from("X"))
)
},
])
);
assert_eq!(
test_build_ast("start: LET X = 0\nmultiply: LET Y = 2 * X\n"),
BasicAstBlock::new([
BasicAstInstruction {
label: Some(String::from("start")),
operation: BasicAstOperation::Assign(
String::from("X"),
BasicAstExpression::Integer(0)
)
},
BasicAstInstruction {
label: Some(String::from("multiply")),
operation: BasicAstOperation::Assign(
String::from("Y"),
BasicAstExpression::Integer(2)
* BasicAstExpression::Variable(String::from("X"))
)
},
])
);
}

@ -40,6 +40,7 @@ pub enum BasicToken {
Else,
EndIf,
Goto,
LabelEnd,
OpenParen,
CloseParen,
Integer(i64),
@ -111,6 +112,7 @@ pub fn tokenize(raw: &str) -> Result<Vec<BasicToken>, ParseError> {
let match_integer = Regex::new(r"^[0-9]+").unwrap();
let match_assign = Regex::new(r"^=").unwrap();
let match_operator = Regex::new(r"^(?:[+\-*/%]|[<>]=?|[!=]=|<<|>>)").unwrap();
let match_label_end = Regex::new(r"^:").unwrap();
let match_paren = Regex::new(r"^(?:\(|\))").unwrap();
// TODO: handle escapes
let match_string = Regex::new(r#""[^"]*""#).unwrap();
@ -153,6 +155,7 @@ pub fn tokenize(raw: &str) -> Result<Vec<BasicToken>, ParseError> {
">>" => Operator::RShift,
_ => unreachable!(),
})),
match_label_end => (BasicToken::LabelEnd),
match_paren(paren) => (if paren == "(" {
BasicToken::OpenParen
} else {

Loading…
Cancel
Save