From 7d219a98bd89dcade347c4b1adf4a1f6d5415ef6 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Mon, 21 Oct 2024 14:24:59 +0200 Subject: Allow for .byte/.address statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miquel Sabaté Solà --- lib/xixanta/src/assembler.rs | 147 ++++++++++++++++++++++++++++++++++++------- lib/xixanta/src/context.rs | 26 +++++--- 2 files changed, 143 insertions(+), 30 deletions(-) (limited to 'lib/xixanta') diff --git a/lib/xixanta/src/assembler.rs b/lib/xixanta/src/assembler.rs index aa0db19..0425ede 100644 --- a/lib/xixanta/src/assembler.rs +++ b/lib/xixanta/src/assembler.rs @@ -60,6 +60,7 @@ pub struct Assembler { literal_mode: Option, stage: Stage, macros: HashMap, + can_bundle: bool, } impl Assembler { @@ -78,6 +79,7 @@ impl Assembler { literal_mode: None, stage: Stage::Init, macros: HashMap::new(), + can_bundle: true, } } @@ -187,12 +189,11 @@ impl Assembler { pub fn bundle(&mut self, nodes: &Vec) -> Result, Vec> { let mut bundles = Vec::new(); let mut errors = Vec::new(); - let mut inside_macro = false; for node in nodes { match node.node_type { NodeType::Instruction => { - if !inside_macro { + if self.can_bundle { match self.evaluate_node(node) { Ok(bundle) => bundles.push(bundle), Err(e) => errors.push(Error::Eval(e)), @@ -200,23 +201,8 @@ impl Assembler { } } NodeType::Control => { - let id = node.value.value.as_str(); - - // TODO: skip macros altogether. - if id == ".macro" { - if inside_macro { - errors.push(Error::Eval(EvalError { - message: "nesting macros is forbidden".to_string(), - line: node.value.line, - })); - continue; - } - inside_macro = true; - } else if id == ".endmacro" { - inside_macro = false; - } else if let Err(err) = self.context.change_context(node) { - // TODO: forbid if inside_macro - errors.push(Error::Context(err)); + if let Err(e) = self.evaluate_control_statement(node, &mut bundles) { + errors.push(Error::Eval(e)); } } NodeType::Value | NodeType::Call => { @@ -301,7 +287,7 @@ impl Assembler { match node.node_type { NodeType::Instruction => Ok(self.evaluate_instruction(node)?), NodeType::Literal => Ok(self.evaluate_literal(node)?), - NodeType::Control => Ok(self.evaluate_control(node)?), + NodeType::Control => Ok(self.evaluate_control_expression(node)?), NodeType::Value => match self.literal_mode { Some(LiteralMode::Hexadecimal) => Ok(self.evaluate_hexadecimal(node)?), Some(LiteralMode::Binary) => Ok(self.evaluate_binary(node)?), @@ -583,7 +569,38 @@ impl Assembler { } } - fn evaluate_control(&mut self, node: &PNode) -> Result { + fn evaluate_control_statement( + &mut self, + node: &PNode, + res: &mut Vec, + ) -> Result<(), EvalError> { + let changed; + + // This might just be a statement that changes the context (e.g. + // ".macro", ".proc", etc.). In this case change the context and leave + // early. + (changed, self.can_bundle) = self.context.change_context(node)?; + if changed { + return Ok(()); + } + + // Otherwise, check the function that could act as a statement that + // produces bundles. + let function = node.value.value.as_str(); + match function { + ".byte" | ".db" => self.push_evaluated_arguments(node, res, 1), + ".addr" | ".dw" => self.push_evaluated_arguments(node, res, 2), + _ => Err(EvalError { + line: node.value.line, + message: format!( + "cannot handle control statement '{}' in this context", + function + ), + }), + } + } + + fn evaluate_control_expression(&mut self, node: &PNode) -> Result { let function = node.value.value.as_str(); match function { @@ -622,6 +639,58 @@ impl Assembler { Ok(bundle) } + fn push_evaluated_arguments( + &mut self, + node: &PNode, + res: &mut Vec, + nbytes: u8, + ) -> Result<(), EvalError> { + match &node.args { + Some(args) => { + for arg in args { + // Evaluate the argument as a node. + let mut bundle = self.evaluate_node(arg)?; + + // If there is a missmatch between the expected number of + // bytes and what we got, we might be able to resolve it if + // we expected two bytes and only one was received: extend + // it by leading zeroes. Otherwise we have to error out: + // it's up to the programmer to either call `.hibyte` or + // something similar if that's whay they intended. + if bundle.size != nbytes { + match nbytes { + 1 => { + return Err(EvalError { + line: arg.value.line, + message: "expecting an argument that fits into a byte" + .to_string(), + }) + } + 2 => { + bundle.size = 2; + bundle.bytes[1] = 0x00; + bundle.bytes[2] = 0x00; + } + _ => panic!("bad argument when evaluating arguments"), + } + } + res.push(bundle); + } + } + None => { + return Err(EvalError { + line: node.value.line, + message: format!( + "expecting at least one argument for '{}'", + node.value.value.as_str(), + ), + }) + } + } + + Ok(()) + } + fn evaluate_variable(&mut self, id: &PString) -> Result { match self.context.get_variable(id) { Ok(value) => Ok(value), @@ -1322,7 +1391,41 @@ lda #Scope::Variable // TODO // Control statements - // TODO: .byte, .word, variables in between (e.g. `.byte Variable::Value`, `lda #.hibyte(Variable)`) + + #[test] + fn byte_literals() { + let mut asm = Assembler::new(EMPTY.to_vec()); + let res = asm + .assemble( + r#" +.scope Vars + Variable = 4 +.endscope + +.byte #Vars::Variable +.dw $2001, $02 +"# + .as_bytes(), + ) + .unwrap(); + + assert_eq!(res.len(), 3); + + // .byte + assert_eq!(res[0].bytes[0], 0x04); + assert_eq!(res[0].bytes[1], 0x00); + assert_eq!(res[0].size, 1); + + // First .dw argument. + assert_eq!(res[1].bytes[0], 0x01); + assert_eq!(res[1].bytes[1], 0x20); + assert_eq!(res[1].size, 2); + + // Second .dw argument. + assert_eq!(res[2].bytes[0], 0x02); + assert_eq!(res[2].bytes[1], 0x00); + assert_eq!(res[2].size, 2); + } #[test] fn hi_lo_byte() { diff --git a/lib/xixanta/src/context.rs b/lib/xixanta/src/context.rs index 8e77094..3fceab9 100644 --- a/lib/xixanta/src/context.rs +++ b/lib/xixanta/src/context.rs @@ -87,8 +87,10 @@ impl Context { Ok(()) } - /// Change the current context given a `node`. - pub fn change_context(&mut self, node: &PNode) -> Result<(), ContextError> { + /// Change the current context given a `node`. Returns a tuple which states: + /// 0. Whether the context has changed. + /// 1. Whether a caller can bundle nodes safely. + pub fn change_context(&mut self, node: &PNode) -> Result<(bool, bool), ContextError> { // The parser already guarantees that the control node is // from a function that we already know, so calling `unwrap` // is not dangerous. @@ -98,17 +100,25 @@ impl Context { // If the control function does not touch the context, leave early. if !control.touches_context { - return Ok(()); + return Ok((false, true)); } // And push/pop the context depending on the control being used. match node.value.value.as_str() { - ".macro" | ".proc" | ".scope" => self.context_push(&node.left.clone().unwrap()), - ".endmacro" | ".endproc" | ".endscope" => self.context_pop(&node.value)?, - _ => {} + ".macro" => { + self.context_push(&node.left.clone().unwrap()); + Ok((true, false)) + } + ".proc" | ".scope" => { + self.context_push(&node.left.clone().unwrap()); + Ok((true, true)) + } + ".endmacro" | ".endproc" | ".endscope" => { + self.context_pop(&node.value)?; + Ok((true, true)) + } + _ => Ok((false, true)), } - - Ok(()) } // Pushes a new context given a `node`, which holds the identifier of the -- cgit v1.2.3