diff --git a/src/parse/ast.rs b/src/parse/ast.rs index a4ff12c..1db93dc 100644 --- a/src/parse/ast.rs +++ b/src/parse/ast.rs @@ -9,6 +9,24 @@ pub enum BasicAstExpression { Binary(Operator, Box, Box), } +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 for BasicAstInstruction { + fn from(operation: BasicAstOperation) -> Self { + Self { + label: None, + operation, + } + } +} + #[derive(Clone, Debug, PartialEq, Default)] pub struct BasicAstBlock { pub instructions: Vec, } +impl BasicAstBlock { + pub fn new(instructions: impl IntoIterator) -> 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 { tokens @@ -122,13 +157,13 @@ pub fn build_ast(tokens: &[BasicToken]) -> Result { let mut current_label: Option = 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 { }); } - 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())); } } diff --git a/src/parse/test.rs b/src/parse/test.rs index b35e912..b728bd7 100644 --- a/src/parse/test.rs +++ b/src/parse/test.rs @@ -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")) + ) + }, + ]) + ); +} diff --git a/src/parse/tokenize.rs b/src/parse/tokenize.rs index 3c28cf0..ca1591f 100644 --- a/src/parse/tokenize.rs +++ b/src/parse/tokenize.rs @@ -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, 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, ParseError> { ">>" => Operator::RShift, _ => unreachable!(), })), + match_label_end => (BasicToken::LabelEnd), match_paren(paren) => (if paren == "(" { BasicToken::OpenParen } else {