aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/nasm/README.md63
-rw-r--r--lib/xixanta/src/assembler.rs37
-rw-r--r--lib/xixanta/src/node.rs9
-rw-r--r--lib/xixanta/src/object.rs12
-rw-r--r--lib/xixanta/src/parser.rs57
-rwxr-xr-xscripts/test-e2e.sh7
-rw-r--r--tests/expected/global-labels.nesbin0 -> 32 bytes
-rw-r--r--tests/expected/global-labels.txt0
-rw-r--r--tests/global-labels.s26
9 files changed, 190 insertions, 21 deletions
diff --git a/crates/nasm/README.md b/crates/nasm/README.md
index f96e0bb..1cfea8f 100644
--- a/crates/nasm/README.md
+++ b/crates/nasm/README.md
@@ -98,6 +98,69 @@ missing in `ca65`. In this case, `nasm` has the `--prelude` flag, which will
print some code that can fill the gaps when running `ca65` with some
nasm-specific code. See below.
+#### Defining global labels
+
+For optimization reasons, sometimes it's necessary to write a label that can be
+accessed globally, regardless of the current scope. The
+[jetpac.nes](https://git.mssola.com/nes/jetpac.nes/) game has a good example of
+this need on its `enemies.s` file. In there, we define a function pointer that
+is set to the current function handler for the enemies' algorithm. Then enemy
+handling can go along like this:
+
+```asm
+ ;; previous code
+
+ lda #.hibyte(@return_from_movement_handler - 1)
+ pha
+ lda #.lobyte(@return_from_movement_handler - 1)
+ pha
+ jmp (zp_movement_fn)
+
+@return_from_movement_handler:
+ ;; Rest of the code after enemy movement has been handled.
+```
+
+That is, we push onto the stack the address of `@return_from_movement_handler`,
+and then `jmp` to the function handler. Then, the function handler can simply
+call `rts` and everything will be fine. As explained in the source code, other
+techniques like trampolines or the "rts trick" were not desirable on this
+context.
+
+Moreover, note that a simple `jmp` was not possible from the context of enemy
+handlers, as `@return_from_movement_handler` is inside of the scope of the
+`Enemies::update` proc. For this game the performance penalty with this setup
+was acceptable, but note that it's inside of a loop, and these extra cycles are
+given for each enemy.
+
+But this is potentially bad if your game is more tight on the cycle count and
+every cycle you can squeeze matters. For this reason, `nasm` has the possibility
+to define labels globally. That is, the above example could be rewritten like
+this:
+
+```asm
+ jmp (zp_movement_fn)
+
+#@return_from_movement_handler:
+ ;; Rest of the code after enemy movement has been handled.
+```
+
+Notice the leading '#' symbol. This tells `nasm` that
+`@return_from_movement_handler` should be defined at the global scope,
+regardless of the current one (hence, it's no longer hidden inside of the
+`Enemies::update` scope). With this, then each handler can switch from an `rts`
+to:
+
+```asm
+ jmp @return_from_movement_handler ; or hide it on an 'RTS_FROM_ENEMY_HANDLER' macro.
+```
+
+In total, for each iteration loop, the setup would save 10 cycles (2 cycles x
+lda, 3 cycles x pha), and replacing each handler's `rts` with a `jmp` would save
+3 cycles (6 cycles 'rts' - 3 cycles 'jmp'). That is, given that this game
+allowed 4 enemies at once, then each call to `Enemies::update` could save 13
+cycles x 4 enemies = 52 cycles. Not a crazy amount, yes, but this optimization
+is now so easy to pull that it's well worth it.
+
#### The `__fallthrough__` pseudo-instruction
It's a [well-known
diff --git a/lib/xixanta/src/assembler.rs b/lib/xixanta/src/assembler.rs
index 91f1e14..3cf1e0b 100644
--- a/lib/xixanta/src/assembler.rs
+++ b/lib/xixanta/src/assembler.rs
@@ -523,7 +523,10 @@ impl<'a> Assembler<'a> {
accessed: 1,
};
- if let Err(err) = self.context.set_variable(&var_name, &var_value, false) {
+ if let Err(err) = self
+ .context
+ .set_variable(&var_name, &var_value, false, false)
+ {
Err(Error {
global: true,
line: 0,
@@ -557,6 +560,7 @@ impl<'a> Assembler<'a> {
accessed: 0,
},
false,
+ matches!(node.node_type, NodeType::Label(true)),
) {
return Err(Error {
message,
@@ -585,7 +589,7 @@ impl<'a> Assembler<'a> {
// context so it's known. The actual value cannot be computed
// right now as we don't know the segment size where it belongs
// yet.
- NodeType::Label => {
+ NodeType::Label(_) => {
// There's no good reason to declare a named label inside of
// a macro. If that's the case, just error out.
if (self.macros_seen > 0 || self.repeats_seen > 0) && !node.value.is_empty() {
@@ -689,6 +693,7 @@ impl<'a> Assembler<'a> {
accessed: 0,
},
false,
+ false,
) {
errors.push(Error {
message: err,
@@ -909,7 +914,12 @@ impl<'a> Assembler<'a> {
};
if !node.value.is_empty()
- && let Err(message) = self.context.set_variable(&node.value, &object, true)
+ && let Err(message) = self.context.set_variable(
+ &node.value,
+ &object,
+ true,
+ matches!(node.node_type, NodeType::Label(true)),
+ )
{
return Err(Error {
message,
@@ -938,7 +948,7 @@ impl<'a> Assembler<'a> {
// segment. Note that this is only the offset from the beginning
// of the offset, the effective address will only be available
// after calling `Context::get_variable`
- NodeType::Label => {
+ NodeType::Label(_) => {
if let Err(e) = self.apply_segment_offset_to_label(node, ObjectType::Address) {
errors.push(e);
}
@@ -1150,11 +1160,16 @@ impl<'a> Assembler<'a> {
// value.
let mut dobj = d.obj.clone();
dobj.bundle = o;
- self.context.set_variable(&d.id, &dobj, true)
+ self.context.set_variable(
+ &d.id,
+ &dobj,
+ true,
+ matches!(n.node_type, NodeType::Label(true)),
+ )
}
// No attached node, we can proceed by setting the
// variable as is.
- None => self.context.set_variable(&d.id, &d.obj, true),
+ None => self.context.set_variable(&d.id, &d.obj, true, false),
};
}
}
@@ -1714,7 +1729,7 @@ impl<'a> Assembler<'a> {
// calls, just in case a macro is applied multiple times and we
// need to get the latest value.
let id = &margs.next().unwrap().value;
- if let Err(message) = self.context.set_variable(id, &obj, true) {
+ if let Err(message) = self.context.set_variable(id, &obj, true, false) {
return Err(Error {
line: node.value.line,
message,
@@ -2618,6 +2633,7 @@ impl<'a> Assembler<'a> {
accessed: 0,
},
true,
+ false,
)
{
return Err(Error {
@@ -3099,7 +3115,12 @@ impl<'a> Assembler<'a> {
// again. If the variable could not be set, then it's not
// that big of a deal at this stage.
value.bundle = bundle.clone();
- let _ = self.context.set_variable(&node.value, &value, true);
+ let _ = self.context.set_variable(
+ &node.value,
+ &value,
+ true,
+ matches!(node.node_type, NodeType::Label(true)),
+ );
// And return the computed bundle.
Ok(bundle)
diff --git a/lib/xixanta/src/node.rs b/lib/xixanta/src/node.rs
index 649f333..357b9e0 100644
--- a/lib/xixanta/src/node.rs
+++ b/lib/xixanta/src/node.rs
@@ -319,8 +319,10 @@ pub enum NodeType {
/// '$'. The `left` node contains the inner expression.
Literal,
- /// A label statement, which only sets the `value`, the name of the label.
- Label,
+ /// A label statement, which only sets the `value`, the name of the
+ /// label. If the inner boolean is true, then the label is considered to be
+ /// global, otherwise scopes apply.
+ Label(bool),
/// A macro call. Note that a Value might also encode this, but when a Call
/// has been detected, then there is no doubt on it.
@@ -347,7 +349,8 @@ impl fmt::Display for NodeType {
NodeType::Control(control_type) => write!(f, "control function ({control_type})"),
NodeType::ControlBody => write!(f, "control function body"),
NodeType::Literal => write!(f, "literal"),
- NodeType::Label => write!(f, "label"),
+ NodeType::Label(true) => write!(f, "(global) label"),
+ NodeType::Label(false) => write!(f, "(scoped) label"),
NodeType::Call => write!(f, "call"),
NodeType::Fallthrough => write!(f, "fallthrough"),
NodeType::Operation(op) => match op {
diff --git a/lib/xixanta/src/object.rs b/lib/xixanta/src/object.rs
index 6209afd..cc8908c 100644
--- a/lib/xixanta/src/object.rs
+++ b/lib/xixanta/src/object.rs
@@ -329,14 +329,22 @@ impl Context {
/// Sets a value for an object identified by `id`. If `overwrite` is set to
/// true, then this value will be set even if the id already existed,
- /// otherwise it will return a ContextError
+ /// otherwise it will return a ContextError. If 'force_global' is set to
+ /// true, then the variable will be defined at the global context, not the
+ /// current one.
pub fn set_variable(
&mut self,
id: &PString,
object: &Object,
overwrite: bool,
+ force_global: bool,
) -> Result<(), String> {
- let scope_name = self.name().to_string();
+ let scope_name = if force_global {
+ GLOBAL_CONTEXT
+ } else {
+ self.name()
+ }
+ .to_string();
let scope = self.map.get_mut(&scope_name).unwrap();
match scope.get_mut(&id.value) {
diff --git a/lib/xixanta/src/parser.rs b/lib/xixanta/src/parser.rs
index abcf1e8..a71c6f6 100644
--- a/lib/xixanta/src/parser.rs
+++ b/lib/xixanta/src/parser.rs
@@ -536,7 +536,7 @@ impl Parser {
// identifier and finally fall through.
self.offset = 0;
let (mut id, mut nt) = self.parse_identifier(l, true)?;
- if nt == NodeType::Label {
+ if matches!(nt, NodeType::Label(_)) {
self.nodes.last_mut().unwrap().push(PNode {
node_type: nt,
value: id,
@@ -558,7 +558,7 @@ impl Parser {
// for it and fall through.
self.offset = 0;
(id, nt) = self.parse_identifier(l, true)?;
- if nt == NodeType::Label {
+ if matches!(nt, NodeType::Label(_)) {
return Err(self
.parser_error("cannot have multiple labels at the same location")
.into());
@@ -624,10 +624,11 @@ impl Parser {
// statement. Returns a PString representing this identifier on success,
// plus a hint on whether the identifier belongs to a label or not.
fn parse_identifier(&mut self, line: &str, dot: bool) -> Result<(PString, NodeType), Error> {
- let start = self.column;
- let base_offset = self.offset;
+ let mut start = self.column;
+ let mut base_offset = self.offset;
let mut nt = NodeType::Value;
let mut first_seen = false;
+ let mut global_label = false;
// For the general case we just need to iterate until a whitespace
// character or an inline comment is found. Then our PString object is
@@ -645,6 +646,25 @@ impl Parser {
if c == '.' && (!dot || first_seen) {
return Err(self.parser_error("cannot have a '.' in this context"));
}
+
+ // If this is the first character, then allow a leading '#'
+ // character to denote a global label. Note that the '#' symbol will
+ // be skipped in the resulting name, and the start/end positions
+ // from the node will also reflect the '#' symbol being skipped.
+ if c == '#' && !first_seen {
+ global_label = true;
+
+ // We don't want the '#' symbol to be considered part of the
+ // name, as it's just a literal from the syntax, not the name
+ // itself.
+ base_offset += 1;
+ start += 1;
+
+ // Nothing else to be done, just jump into the next character.
+ first_seen = true;
+ self.next();
+ continue;
+ }
first_seen = true;
// Check for the end of the identifier. For this, it's easier to
@@ -732,18 +752,26 @@ impl Parser {
}
}
// Regular label (e.g. "label:").
- _ => nt = NodeType::Label,
+ _ => nt = NodeType::Label(global_label),
}
}
}
// If there are no characters left, check if it was a regular label.
None => {
if c == ':' {
- nt = NodeType::Label;
+ nt = NodeType::Label(global_label);
}
}
}
+ // Detect bogus names starting with '#' but not being an actual
+ // label.
+ if global_label && !matches!(nt, NodeType::Label(true)) {
+ return Err(
+ self.parser_error("identifier starts with a '#' but it's not a label")
+ );
+ }
+
// The value of the identifier is whatever we have picked up
// along the parsing.
let value = String::from(line.get(base_offset..self.offset).unwrap_or("").trim());
@@ -755,7 +783,7 @@ impl Parser {
// regular labels because we want to ignore to extra ':'
// character in the end.
let end = match nt {
- NodeType::Label => {
+ NodeType::Label(_) => {
self.next();
self.column - 1
}
@@ -1729,7 +1757,7 @@ impl Parser {
return Err(self.parser_error("invalid identifier"));
}
- if nt == NodeType::Label {
+ if matches!(nt, NodeType::Label(_)) {
Err(self.parser_error("not expecting a label defined here"))
} else {
Ok(PNode {
@@ -2219,6 +2247,19 @@ mod tests {
assert_node(nodes.last().unwrap(), NodeType::Instruction, line, "dex")
}
+ #[test]
+ fn bad_pound_label() {
+ let mut parser = Parser::default();
+ let err = parser
+ .parse("#fakelabel = 1".as_bytes(), &SourceInfo::default())
+ .unwrap_err();
+
+ assert_eq!(
+ err.first().unwrap().message,
+ "identifier starts with a '#' but it's not a label"
+ );
+ }
+
// Literals
#[test]
diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh
index 0620f25..6e18644 100755
--- a/scripts/test-e2e.sh
+++ b/scripts/test-e2e.sh
@@ -140,6 +140,13 @@ exit_code=$((exit_code + $?))
diff tests/out/fixed-segments.nes tests/expected/fixed-segments.nes
exit_code=$((exit_code + $?))
+echo "test: custom => global-labels.nes"
+./target/debug/nasm -c empty -Werror --asan -o tests/out/global-labels.nes tests/global-labels.s 2>tests/out/global-labels.txt
+diff tests/out/global-labels.txt tests/expected/global-labels.txt
+exit_code=$((exit_code + $?))
+diff tests/out/global-labels.nes tests/expected/global-labels.nes
+exit_code=$((exit_code + $?))
+
##
# code.nes
diff --git a/tests/expected/global-labels.nes b/tests/expected/global-labels.nes
new file mode 100644
index 0000000..3b24857
--- /dev/null
+++ b/tests/expected/global-labels.nes
Binary files differ
diff --git a/tests/expected/global-labels.txt b/tests/expected/global-labels.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/expected/global-labels.txt
diff --git a/tests/global-labels.s b/tests/global-labels.s
new file mode 100644
index 0000000..93b4d6d
--- /dev/null
+++ b/tests/global-labels.s
@@ -0,0 +1,26 @@
+.segment "HEADER"
+ .byte 'N', 'E', 'S', $1A
+ .byte $02, $01
+ .byte $00
+ .byte $00
+
+.segment "CODE"
+;; asan:stack full
+
+.scope Scope
+ .proc foo
+ lda #1 ; $8000
+ jmp Scope::bar ; $8002
+ #@known_address:
+ rts ; $8005
+ .endproc
+
+ .proc bar
+ jmp @label ; $8006
+ nop ; $8009
+ @label:
+ jmp @known_address ; $800A
+ .endproc
+.endscope
+
+jsr Scope::foo ; $800D