aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/custom.md17
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md10
-rw-r--r--.github/workflows/ci.yml22
-rw-r--r--.gitignore3
-rw-r--r--CHANGELOG.md6
-rw-r--r--CONTRIBUTING.md24
-rw-r--r--LICENSE165
-rw-r--r--Makefile43
-rw-r--r--README.md202
-rw-r--r--examples/mul.s81
-rw-r--r--list.s179
-rw-r--r--test/common.s83
-rw-r--r--test/run.sh56
-rw-r--r--test/suite.lua66
-rw-r--r--test/suite.s186
-rw-r--r--test/utils.lua80
16 files changed, 1223 insertions, 0 deletions
diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md
new file mode 100644
index 0000000..4a0af7e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/custom.md
@@ -0,0 +1,17 @@
+---
+name: Custom issue template
+about: General issues, both bugs and features.
+title: ''
+labels: ''
+assignees: mssola
+---
+
+### Description
+
+1. First I did this...
+2. Then that...
+3. And this happened!
+4. Versions: git commit sha, toolchain version, version of the emulator/system, etc.
+
+- **Expected behavior**: I expected this to happen!
+- **Actual behavior**: But this happened...
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..13ff63b
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,10 @@
+Provide a general description of the changes in your pull request. If this pull
+request fixes a known issue, please tag it as well (e.g.: `Fixes #1`). In any
+case:
+
+- [ ] Write a [good commit message](https://chris.beams.io/posts/git-commit/).
+- [ ] You are sure that `make` continues to work.
+- [ ] The pull request has *only* one subject and a clear title. You are not
+ submitting a pull request with tons of different unrelated commits.
+
+Thanks!
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c590235
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,22 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ name: Continuous Integration
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Install dependencies from Ubuntu sources
+ run: sudo apt-get install cc65 fceux ruby
+
+ - name: Main task
+ run: make
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e1dbaac
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+tmp/
+*.o
+**/*.nes
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d433fdc
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+## 0.1
+
+Initial version with a `list.s` file which implements the `List` scope. This
+scope defines two 16-bit pointers from which you can manipulate a list, but they
+are more useful when used together with the macros `LIST_INIT`, `LIST_IT_FROM`
+and `LIST_NEXT`; and the functions `set`, `push` and `get`.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0baab7f
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,24 @@
+## Why?
+
+Do you want to fix an error you have found? Do you know a way to improve my
+6502-fu or do you know a technique on NES development that might help here? I am
+open for discussion and welcome any help!
+
+## How?
+
+There are many ways to help me out. One way might be to open an issue on
+[Github's tracker](https://github.com/mssola/list.nes/issues) and start a
+discussion. For this, mind the following:
+
+- Check that the issue has not already been reported or fixed in `main`.
+- Try to be concise and precise in your description.
+- If you have found a problem, provide a step by step guide on how to reproduce it.
+- Provide the version you are using (git commit SHA), as well as the version of
+ the toolchain and the emulator/system being used.
+
+Another way is to simply submit a pull request. For this, also mind these:
+
+- Write a [good commit message](https://chris.beams.io/posts/git-commit/).
+- You are sure that `make` continues to work.
+- The pull request has *only* one subject and a clear title. You are not
+ submitting a pull request with tons of different unrelated commits.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0a04128
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..ef3cb0e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,43 @@
+CA65 ?= ca65
+CC65 ?= cl65
+CCOPTS ?= --target nes
+ifeq "$(DEBUG)" "1"
+CCOPTS += -g -Ln tmp/labels.txt
+endif
+
+
+.PHONY: test
+test: prepare build example unit
+
+.PHONY: prepare
+prepare: clean deps
+
+.PHONY: clean
+clean:
+ @sed -i '/RUN_TESTS = 1/d' test/suite.s
+ @rm -rf tmp
+ @mkdir tmp
+ @find . -type f -name "*.o" -delete
+ @find . -type f -name "*.nes" -delete
+
+.PHONY: deps
+deps:
+ @which $(CC65) >/dev/null 2>/dev/null || (echo "ERROR: $(CC65) not found." && false)
+ @which fceux >/dev/null 2>/dev/null || (echo "ERROR: fceux not found." && false)
+
+.PHONY: build
+build: list.o
+
+%.o: %.s
+ $(CA65) $< -o $@
+
+test/%.nes: test/%.s
+ $(CC65) $(CCOPTS) $< -o $@
+
+.PHONY: unit
+unit:
+ @bash test/run.sh
+
+.PHONY: example
+example:
+ $(CC65) $(CCOPTS) examples/mul.s -o examples/mul.nes
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..982c91a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,202 @@
+## list.nes
+
+This is a small library that allows NES developers to maintain and
+manipulate big lists on the system.
+
+The use case for this is indeed rare for NES games, where you usually have an
+already defined table or an array with a fixed small size (or at least not
+huge). That being said, I encountered this problem while solving [day 4 of
+Advent of Code 2023](https://adventofcode.com/2023/day/4) (see my repo
+[mssola/aoc2023.nes](https://github.com/mssola/aoc2023.nes)), in which I needed
+to manipulate a list of around 600 bytes. This was a bit tricky to get right and
+it added a bunch of code that was unrelated to the challenge itself. Hence, I
+extracted the logic into this library and tuned things down from the lessons I
+learnt along the way.
+
+It's all pretty simple but being assembly code it comes with some gotchas that
+you need to be aware of.
+
+### Installing
+
+You need to copy the code into some vendor directory, or add this repository as
+a git submodule. After doing that, simply `.include` it in your code and it's
+ready to be used.
+
+As for the memory mapping, please do note that this library needs 4 bytes in
+order to store two 16-bit pointers that keep track of the list. By default these
+4 bytes are located at \$60-\$63. If that clashes with something from your code,
+simply change these variables, named `List::ptr` and `List::last`. Each of these
+pointers are 16-bit long (hence 2 bytes long), and they don't need to be in
+continguous memory locations.
+
+### Creating a list
+
+In order to create a list you don't need to reserve a whole memory location in a
+fixed way. Rather, what you do is to set the `List::ptr` and the `List::last`
+pointers to the beginning of the memory from which your list will grow. Thus,
+it's up to you to set this to a valid memory address which is free for as many
+bytes as you'll need. This library will not try to avoid possible clashes and
+will simply grow mindlessly as long as you keep pushing data to it.
+
+And so, what are these pointers? `List::ptr` stores the memory location in
+little endian format in which the current read/write will happen, while
+`List::last` stores the last memory address in which a push happened. That is,
+`List::last` resolves to how big your list is, while `List::ptr` is simply an
+iterator over this list. Hence, in order to "create" a list you have to set both
+these pointers to the same memory address, or simply use the provided
+`LIST_INIT` macro, which asks for a full 16-bit address:
+
+``` assembly
+LIST_INIT $0400
+;; now you have a list that starts at memory address $0400.
+```
+
+### Pushing data
+
+The most basic operation from this library is `List::push`, which will push a
+new value to the list and grow it by one byte. Example:
+
+``` assembly
+LIST_INIT $0400
+
+;; The value to be pushed has to be stored into the `a` register.
+lda #2
+jsr List::push
+;; now your list is: [$02] ($0400: $02).
+
+lda #$F0
+jsr List::push
+;; now your list is: [$02, $F0] ($0400: $02, $0401: $F0).
+
+;; and so on...
+```
+
+Note that both the `a` and the `y` registers will be affected after calling
+`List::push`, and that the carry flag might also be set.
+
+### Getting data
+
+Now that you have pushed data, it would be cool to actually get it back! For
+that you have two ways. First of all, you can perform a load with indirect
+addressing by using the `List::ptr` pointer. Hence, the following allows you to
+fetch the item at memory address `$0401`:
+
+``` assembly
+lda #$01
+sta List::ptr
+lda #$04
+sta List::ptr + 1
+
+ldy #0
+lda (List::ptr), y
+```
+
+Note that there is a macro which resets the iterator pointer called
+`LIST_IT_FROM`. Hence, the previous code could have been written like so:
+
+``` assembly
+LIST_IT_FROM $0401
+
+ldy #0
+lda (List::ptr), y
+```
+
+This is convenient for random access, but when you are iterating over the list
+it might be tedious to move the pointer over and over. This is why there is also
+the `LIST_NEXT` macro, which will move the `List::ptr` pointer for you:
+
+``` assembly
+LIST_IT_FROM $0401
+
+ldy #0
+lda (List::ptr), y
+
+LIST_NEXT ;; so `List::ptr` points to $0402.
+```
+
+But doing things like this there is the gotcha that you don't know when to stop
+if you are just iterating over the whole list. In order to solve this problem
+there is another way of fetching data: the `List::get` function. This function
+works like this:
+
+1. Try to fetch the value on `List::ptr`.
+2. If `List::ptr` is already at the end of the list, it will set `a` to `0` and
+ `y` to `$FF`.
+3. If not, then the value will be copied to `a` and `y` will be set to `0`.
+4. Move `List::ptr` to the next element if possible.
+
+This way you can come up with a code like this:
+
+``` assembly
+;; Imagine we have a list already initialized at $0400. Let's reset the `List::ptr`
+;; so to start iterating from there.
+LIST_IT_FROM $0400
+
+@loop:
+ jsr List::get
+ cpy #$FF
+ beq @done
+ ;; do whatever with the value on `a`.
+ jmp @loop
+@done:
+ ;; move on...
+```
+
+This way you don't have to care about pointers or anything: just set where you
+want to start iterating your list, and call `List::get` until `y` has an `$FF`
+value.
+
+### Setting values without growing the list
+
+As we have seen with `List::push`, this subroutine will also move the
+`List::last` pointer, so our list grew on each push. Now let's say that we have
+already initialized a list at `$0400` and we want to set some values without
+growing the list any further. One way would be to, again, just use the
+`List::ptr` variable:
+
+``` assembly
+LIST_IT_FROM $0402
+
+lda #1
+ldy #0
+sta (List::ptr), y
+```
+
+But again, when doing it while iterating this can be a bit tedious. That's why
+there is the `List::set` function. Here you'd have:
+
+``` assembly
+LIST_IT_FROM $0402
+lda #1
+jsr List::set
+;; `List::ptr` has been moved to point to $0403 so you can use it on the next iteration.
+```
+
+And again, this `List::set` function works in the same way as `List::get`, in
+which the `y` register contains whether the operation could be performed or not.
+That is, if we were at the end of the list, then `y` is set to `$FF`, otherwise
+to `0`. This again is pretty convenient when performing a loop:
+
+``` assembly
+;; Imagine we have a list at $0400. Let's reset the `List::ptr` so to start
+;; iterating from there.
+LIST_IT_FROM $0400
+
+@loop:
+ lda #2
+ jsr List::set
+ cpy #$FF
+ bne @loop
+ ;; move on...
+```
+
+### Putting it all together
+
+You can check out an example in the [examples/mul.s](./examples/mul.s) file.
+Otherwise you can also see it being used in real life on my solution for [day 4
+of Advent of Code](https://github.com/mssola/aoc2023.nes/blob/main/src/4.s).
+
+## License
+
+Released under the [LGPLv3+](http://www.gnu.org/licenses/lgpl-3.0.txt),
+Copyright (C) 2023-<i>Ω</i> Miquel Sabaté Solà.
diff --git a/examples/mul.s b/examples/mul.s
new file mode 100644
index 0000000..81e12f4
--- /dev/null
+++ b/examples/mul.s
@@ -0,0 +1,81 @@
+;;;
+;; A silly example that tries to demonstrate how to use list.nes.
+;;
+;; It will initialize a list of 256 16-bit items with a value of 0..255
+;; respectively. After doing that, it will multiply each value by 2 and store it
+;; again. As a final touch, the second item will be forced to have the same
+;; value for the high and the low bytes.
+;;
+;; You can check that this works by running `make example` and then running the
+;; resulting ROM file into an emulator with a RAM inspector like FCEUX or Mesen.
+;; Then go from memory address $0400 onwards and check that the results are as
+;; described.
+
+.include "../test/common.s"
+
+main:
+ LIST_INIT $0400
+
+ ;; We will store 256 16-bit items and $90 will keep track of it.
+ lda #0
+ sta $90
+@fill_loop:
+ ;; The 16-bit item will have the value of the current index as stored in
+ ;; $90. The list will be initialized through the `List::push` function, so
+ ;; the size is properly set at the end.
+ lda $90
+ jsr List::push
+ lda #0
+ jsr List::push
+
+ ;; If we have set all the items we wanted, move into the next thing,
+ ;; otherwise increase $90 and go back into the loop.
+ lda $90
+ cmp #$FF
+ beq @set
+ inc $90
+ jmp @fill_loop
+
+@set:
+ ;; Reset the pointer to the start of the list since we want to iterate it
+ ;; over.
+ LIST_IT_FROM $0400
+
+@set_loop:
+ ;; There are two values to keep track: `List::ptr` and `List::ptr + 1`. For
+ ;; the low byte we need to shift left once, and then the high byte needs to
+ ;; add the possible carry to itself. In order to do this and call
+ ;; `List::set` properly we need to be do some stuff with the stack (low
+ ;; byte) or the `x` register (high byte).
+ ldy #0
+ lda (List::ptr), y
+ asl
+ pha
+
+ lda #0
+ iny
+ adc (List::ptr), y
+ tax
+
+ ;; Pull the value that corresponds to the low byte and set it. After doing
+ ;; that, though, did we get a $FF value? If so then it means that we are
+ ;; done with it, otherwise proceed to grab the high byte stored on the `x`
+ ;; registerm set it with `List::set` and loop again.
+ pla
+ jsr List::set
+ cpy #$FF
+ beq @final_get
+ txa
+ jsr List::set
+ jmp @set_loop
+
+@final_get:
+ ;; We are done! Now for a final touch let's copy the value from the low byte
+ ;; into the high byte of the second element.
+ LIST_IT_FROM $0402
+
+ jsr List::get
+ jsr List::set
+
+done:
+ jmp done
diff --git a/list.s b/list.s
new file mode 100644
index 0000000..8294b84
--- /dev/null
+++ b/list.s
@@ -0,0 +1,179 @@
+;;;
+;; list.nes - a small library to maintain big lists on the NES.
+;;
+;; Copyright (C) Miquel Sabaté Solà <mikisabate@gmail.com>
+;;
+;; This library is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU Lesser General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; This library is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU Lesser General Public License for more details.
+;;
+;; You should have received a copy of the GNU Lesser General Public License
+;; along with this library. If not, see <https://www.gnu.org/licenses/>.
+
+.p02
+
+;; Initialize a new list which starts at the given 16-bit address.
+.macro LIST_INIT address
+ lda #.lobyte(address)
+ sta List::ptr
+ sta List::last
+ lda #.hibyte(address)
+ sta List::ptr + 1
+ sta List::last + 1
+.endmacro
+
+;; For an already initialized list, reset the `List::ptr` variable so the list
+;; can be iterated with functions like `List::get`.
+.macro LIST_IT_FROM address
+ lda #.lobyte(address)
+ sta List::ptr
+ lda #.hibyte(address)
+ sta List::ptr + 1
+.endmacro
+
+;; Move the `List::ptr` variable so to point to the next element.
+.macro LIST_NEXT
+ clc
+ lda #1
+ adc List::ptr
+ sta List::ptr
+ lda #0
+ adc List::ptr + 1
+ sta List::ptr + 1
+.endmacro
+
+;;;
+;; List provides the scope for the variables and subroutines that have been
+;; defined in order to maintain and manipulate big lists on the NES.
+;;
+;; This library can also work for smaller list but there are other more
+;; performant ways to achieve the same thing. Thus, use this library only if you
+;; need to have a list that may store more than 255 bytes and indexing in the
+;; usual ways might be a problem otherwise.
+;;
+;; For more information on the usage and the API check the documentation
+;; (README.md file on the https://github.com/mssola/list.nes repository).
+.scope List
+ ;; NOTE (important): this library needs 4 bytes ($60-$63) to store
+ ;; information of this list. These are two 16-bit pointers which are used
+ ;; for the subroutines being defined here. If you have a clash with these
+ ;; memory addresses, do feel free to change them, but remember that the code
+ ;; assumes that both pointers are 16-bit (they don't need to be contiguous).
+ ptr = $60
+ last = $62
+
+ ;; Set the value of `a` into the list and advance one position without
+ ;; growing the list.
+ ;;
+ ;; Use this function instead of `List::push` if you have already set
+ ;; `List::ptr` as desired (e.g. with `LIST_IT_FROM`) and you just want to
+ ;; set a specific value to this address. That is, the list has already been
+ ;; defined somewhere else and you are just modifying some position.
+ ;;
+ ;; If the list pointer is already at the end and the operation is not
+ ;; possible, then `y` is set to $FF, otherwise `y` will be set to 0.
+ ;;
+ ;; NOTE: registers modified: `a` and `y`.
+ .proc set
+ tay
+
+ ;; Check if `List::ptr` >= `List::last` (16-bit comparison). If this is
+ ;; the case, then we are actually done, otherwise we can proceed to set
+ ;; the value as desired.
+ lda List::ptr + 1
+ cmp List::last + 1
+ bcc @do
+ bne @done
+ lda List::ptr
+ cmp List::last
+ bcc @do
+ @done:
+ ;; We were actually done: set `y` to `$FF` to denote "end of list".
+ ldy #$FF
+ lda #0
+ rts
+ @do:
+ tya
+ ldy #0
+ sta (List::ptr), y
+
+ LIST_NEXT
+
+ rts
+ .endproc
+
+ ;; Push the value of `a` after the last position of the list and grow one
+ ;; more byte.
+ ;;
+ ;; Note that this subroutine assumes that we are already at the last item of
+ ;; the list. Hence, if you reset `List::ptr` (e.g. with `LIST_IT_FROM`) and
+ ;; then call this subroutine, you will also move the `List::last` pointer
+ ;; accordingly and thus you might have (accidentally) shrinked the list. If
+ ;; this is not a behavior that you want (you just want to set a value at a
+ ;; specific location), then take a look at `List::set` instead.
+ ;;
+ ;; NOTE: registers modified: `a` and `y`.
+ ;; NOTE: this subroutine might set the carry flag.
+ .proc push
+ ldy #0
+ sta (List::ptr), y
+
+ clc
+ lda #1
+ adc List::ptr
+ sta List::ptr
+ sta List::last
+ lda #0
+ adc List::ptr + 1
+ sta List::ptr + 1
+ sta List::last + 1
+ rts
+ .endproc
+
+ ;; Get the contents of the current position of the list pointer and advance
+ ;; it.
+ ;;
+ ;; The byte will be stored into the 'a' register. If the list pointer is
+ ;; already at the end and won't fetch relevant data, then `y` is set to $FF,
+ ;; otherwise `y` will be set to 0.
+ ;;
+ ;; NOTE: registers modified: 'a' and 'y'.
+ ;; NOTE: this subroutine might set the carry flag.
+ .proc get
+ ;; Check if `List::ptr` >= `List::last` (16-bit comparison). If this is
+ ;; the case, then we are actually done, otherwise we can proceed to
+ ;; fetch the value as desired.
+ lda List::ptr + 1
+ cmp List::last + 1
+ bcc @do
+ bne @done
+ lda List::ptr
+ cmp List::last
+ bcc @do
+ @done:
+ ;; We were actually done: set `y` to `$FF` to denote "end of list" and
+ ;; zero out the returned value.
+ ldy #$FF
+ lda #0
+ rts
+ @do:
+ ;; Fetch the value and push it into the stack since `LIST_NEXT` will
+ ;; actually mess with the `a` register.
+ ldy #0
+ lda (List::ptr), y
+ pha
+
+ LIST_NEXT
+
+ ;; Get the value back from the stack so to set the proper return value.
+ pla
+
+ rts
+ .endproc
+.endscope
diff --git a/test/common.s b/test/common.s
new file mode 100644
index 0000000..f0e9fb9
--- /dev/null
+++ b/test/common.s
@@ -0,0 +1,83 @@
+.segment "HEADER"
+ .byte 'N', 'E', 'S', $1A
+
+ .byte $02
+ .byte $01
+
+ .byte $00
+ .byte $00
+
+.segment "VECTORS"
+ .addr nmi, reset, irq
+
+.segment "CHARS"
+.segment "STARTUP"
+.segment "CODE"
+
+.include "../list.s"
+
+reset:
+ sei
+ cld
+
+ ldx #$40
+ stx $4017
+
+ ldx #$ff
+ txs
+
+ inx
+ stx $2000
+ stx $2001
+ stx $4010
+
+@vblankwait1:
+ bit $2002
+ bpl @vblankwait1
+
+ ldx #0
+ lda #0
+@ram_reset_loop:
+ sta $000, x
+ sta $100, x
+ sta $300, x
+ sta $400, x
+ sta $500, x
+ sta $600, x
+ sta $700, x
+ inx
+ bne @ram_reset_loop
+
+ lda #$ef
+@sprite_reset_loop:
+ sta $200, x
+ inx
+ bne @sprite_reset_loop
+
+ lda #$00
+ sta $2003
+ lda #$02
+ sta $4014
+
+@vblankwait2:
+ bit $2002
+ bpl @vblankwait2
+
+ lda #$3F
+ sta $2006
+ lda #$00
+ sta $2006
+
+ lda #$0F
+ ldx #$20
+@palettes_reset_loop:
+ sta $2007
+ dex
+ bne @palettes_reset_loop
+
+ jmp main
+
+;; Unused
+nmi:
+irq:
+ rti
diff --git a/test/run.sh b/test/run.sh
new file mode 100644
index 0000000..6caef32
--- /dev/null
+++ b/test/run.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+
+set -e
+
+ROOT="$( cd "$( dirname "$0" )/.." && pwd )"
+cd "$ROOT"
+
+# Clean previous builds, prepare the test environment and call build again with
+# a test setup.
+sed -i 's/.ifdef RUN_TESTS/RUN_TESTS = 1\n.ifdef RUN_TESTS/g' "$ROOT/test/suite.s"
+DEBUG=1 make test/suite.nes
+
+rm -f "$ROOT/tmp/test-results.txt"
+
+# Github Actions do not allow GUI programs to be run. This means that the code
+# below will always fail (fceux won't be able to run). There is a way to emulate
+# an X server with tools like xvfb-run or xvncserver, but so far I've had no
+# luck on this front.
+if [ ! -z "${GITHUB_ACTION}" ]; then
+ exit 0
+fi
+
+# Run all the tests that we have on Lua.
+for file in test/*.lua; do
+ if [ -n "$(echo $file | grep -v utils.lua)" ]; then
+ echo $file
+ fceux --loadlua $file "$ROOT/test/suite.nes"
+ fi
+done
+
+#
+# Show the results.
+
+if [ ! -f "$ROOT/tmp/test-results.txt" ]; then
+ echo "Something went wrong: test results were not printed out!"
+ exit 1
+fi
+
+cat "$ROOT/tmp/test-results.txt"
+
+n=$(cat "$ROOT/tmp/test-results.txt" | grep FAIL | wc -l)
+echo ""
+case $n in
+ 0)
+ echo "All tests passed!"
+ exit 0
+ ;;
+ 1)
+ echo "1 test failed!"
+ exit 1
+ ;;
+ *)
+ echo "$n tests failed!"
+ exit 1
+ ;;
+esac
diff --git a/test/suite.lua b/test/suite.lua
new file mode 100644
index 0000000..4a5578c
--- /dev/null
+++ b/test/suite.lua
@@ -0,0 +1,66 @@
+utils = require "utils"
+
+utils.StartRun("Unit tests")
+
+---
+-- We push three elements and we sum them up. We expect a proper sum value
+-- stored at $90, the items on $040{0, 1, 2} and the List pointers at the last
+-- position.
+
+utils.MemTest("@test_list_sum", {
+ {0x60, "03"}, {0x62, "03"}, -- List::{ptr, last} should be at the very end (one past the last written).
+ {0x90, "07"}, -- We pushed three elements: 2, 4, 1; and the sum is left in 0x90.
+ {0x400, "02"}, {0x401, "04"}, {0x402, "01"} -- The three elements pushed by this test.
+})
+
+---
+-- We initialize an empty list and we try to call `get` with no elements. The
+-- pointers should not move and the test function should set a `1` on $90 if
+-- `get` set $FF to `y` (which is what we want).
+
+utils.MemTest("@test_list_empty_get", {
+ {0x60, "00"}, {0x62, "00"}, -- List::{ptr, last} did not advance.
+ {0x90, "01"} -- $FF was simply returned.
+})
+
+---
+-- We initialize a large list ($200-sized) with the 8-bit index as a value
+-- (hence 0-$FF twice). In $90-$91 we leave the sum of all the values.
+
+utils.MemTest("@test_large_list", {
+ {0x90, "00"}, {0x91, "FF"} -- (0..255) * 2 = 65280, which is 0xFF00 in hexadecimal.
+})
+
+---
+-- We run the same code of `@test_list_sum` so to get a new three-sized array,
+-- but then we overwrite the contents. This test makes sure that perform the sum
+-- again gives us a new value and, hence, the previous values were actually
+-- overwritten by `List::set`.
+
+utils.MemTest("@test_list_set", {
+ {0x60, "03"}, {0x62, "03"}, -- List::{ptr, last} should be at the very end (one past the last written).
+ {0x90, "0A"}, -- We overwrote the three elements: 3, 5, 2; and the sum is left in 0x90.
+ {0x400, "03"}, {0x401, "05"}, {0x402, "02"} -- The three elements pushed by this test.
+})
+
+---
+-- We initialize an empty list and we try to call `set` with no elements. The
+-- pointers should not move and the test function should set a `1` on $90 if
+-- `get` set $FF to `y` (which is what we want).
+
+utils.MemTest("@test_list_empty_set", {
+ {0x60, "00"}, {0x62, "00"}, -- List::{ptr, last} did not advance.
+ {0x90, "01"} -- $FF was simply returned.
+})
+
+---
+-- We initialize a list with two elements, and then we try to call `List::set`
+-- three times. The first two writes work, the third not.
+
+utils.MemTest("@test_list_overflow", {
+ {0x60, "02"}, {0x62, "02"}, -- List::{ptr, last} should be at the very end (one past the last written).
+ {0x90, "00"}, {0x91, "00"}, {0x92, "FF"}, -- We overwrote the three elements: 3, 5, 2; and the sum is left in 0x90.
+ {0x400, "02"}, {0x401, "04"}, {0x402, "00"} -- The three elements pushed by this test.
+})
+
+utils.EndRun()
diff --git a/test/suite.s b/test/suite.s
new file mode 100644
index 0000000..c302e0a
--- /dev/null
+++ b/test/suite.s
@@ -0,0 +1,186 @@
+;;;
+;; The definition of `RUN_TESTS` will be inserted automatically when running
+;; tests. Do not insert the definition manually.
+
+.include "common.s"
+
+
+main:
+.ifdef RUN_TESTS
+ jsr suite
+.endif
+halt:
+ jmp halt
+
+;;;
+;; Test suite
+
+suite:
+ jsr list_sum
+ jsr list_empty_get
+ jsr large_list
+ jsr list_set
+ jsr list_empty_set
+ jsr list_set_overflow
+
+ rts
+
+;; This is re-used in both `list_sum` and `list_set`.
+.macro LIST_SUM_AUX
+ lda #0
+ sta $90
+
+ LIST_INIT $0400
+
+ lda #2
+ jsr List::push
+ lda #4
+ jsr List::push
+ lda #1
+ jsr List::push
+
+ LIST_IT_FROM $0400
+:
+ jsr List::get
+ cpy #$FF
+ beq :+
+ clc
+ adc $90
+ sta $90
+ jmp :-
+:
+ nop
+.endmacro
+
+list_sum:
+ LIST_SUM_AUX
+@test_list_sum:
+ rts
+
+list_empty_get:
+ LIST_INIT $0400
+ jsr List::get
+ cpy #$FF
+ beq :+
+ lda #0
+ sta $90
+ jmp @test_list_empty_get
+:
+ lda #1
+ sta $90
+@test_list_empty_get:
+ rts
+
+large_list:
+ lda #0
+ sta $90
+ sta $91
+ sta $92
+
+ LIST_INIT $0400
+
+ ldx #0
+:
+ txa
+ jsr List::push
+
+ ;; Are we about to overflow the `x` register? If so, check if this was the
+ ;; first time or not. If so, then we let it overflow and loop again $FF
+ ;; times. Otherwise we will stop the loop, since we want to have a
+ ;; $200-sized list.
+ cpx #$FF
+ bne :+
+ inc $92
+ lda #2
+ cmp $92
+ beq :++
+:
+ inx
+ jmp :--
+:
+ ;; At this point we have stored this big list, let's add things up.
+ lda #0
+ sta $90
+ sta $91
+ LIST_IT_FROM $0400
+:
+ jsr List::get
+ cpy #$FF
+ beq @test_large_list
+ clc
+ adc $90
+ sta $90
+ lda #0
+ adc $91
+ sta $91
+ jmp :-
+
+@test_large_list:
+ rts
+
+list_set:
+ LIST_SUM_AUX
+
+ lda #0
+ sta $90
+
+ LIST_IT_FROM $0400
+
+ lda #3
+ jsr List::set
+ lda #5
+ jsr List::set
+ lda #2
+ jsr List::set
+
+ LIST_IT_FROM $0400
+:
+ jsr List::get
+ cpy #$FF
+ beq @test_list_set
+ clc
+ adc $90
+ sta $90
+ jmp :-
+@test_list_set:
+ rts
+
+list_empty_set:
+ LIST_INIT $0400
+ jsr List::set
+ cpy #$FF
+ beq :+
+ lda #0
+ sta $90
+ jmp @test_list_empty_set
+:
+ lda #1
+ sta $90
+@test_list_empty_set:
+ rts
+
+list_set_overflow:
+ ;; $0402 should not be set by this function, and this is to be checked; so
+ ;; zero it out before doing anything.
+ lda #0
+ ldy #0
+ sta $0402
+
+ LIST_INIT $0400
+ lda #1
+ jsr List::push
+ lda #2
+ jsr List::push
+
+ LIST_IT_FROM $0400
+ lda #2
+ jsr List::set
+ sty $90
+ lda #4
+ jsr List::set
+ sty $91
+ lda #6
+ jsr List::set
+ sty $92
+@test_list_overflow:
+ rts
diff --git a/test/utils.lua b/test/utils.lua
new file mode 100644
index 0000000..9405c8b
--- /dev/null
+++ b/test/utils.lua
@@ -0,0 +1,80 @@
+utils = {}
+
+-- Returns the root path for the project.
+function utils.RootPath()
+ local fullpath = debug.getinfo(1,"S").source:sub(2)
+ fullpath = io.popen("realpath '"..fullpath.."'", 'r'):read()
+ fullpath = fullpath:gsub('[\n\r]*$','')
+
+ local dirname, filename = fullpath:match('^(.*/)([^/]-)$')
+ dirname = dirname or ''
+ if dirname == '' then
+ return ''
+ end
+
+ return io.popen("realpath '"..dirname.."/..'", 'r'):read()
+end
+
+-- At a `label` that exists on the assembly code grab the values for the given
+-- addresses and write it all into the `test-results.txt` file. The `addresses`
+-- array is made up of two-sized arrays, where the first element contains the
+-- memory you are trying to test, and the second element is the value that we
+-- are expecting.
+function utils.MemTest(label, addresses)
+ local cmd = "cat ".. utils.RootPath() .. "/tmp/labels.txt | grep .".. label .." | awk '{ print $2; }' | cut -c3-"
+ local file = assert(io.popen(cmd, 'r'))
+ local result = file:read("*a")
+
+ -- Double check that the address that we grabbed has at least a good format.
+ if string.len(result) ~= 5 then
+ error("Error on '" .. label .. "': got a bad address! (".. result ..")")
+ end
+
+ -- Register a function to execute on the given test address. The function will
+ -- simply iterate over the given `addresses` and compare them with the
+ -- expected result. Everything will be saved into the `test-results.txt` file.
+ memory.registerexecute(tonumber(result, 16), function()
+ local expected = ""
+ local got = ""
+
+ for _, vals in ipairs(addresses) do
+ expected = expected .. "$" .. string.format("%04X", vals[1]) .. " -> " .. vals[2] .. "; "
+ got = got .. "$" .. string.format("%04X", vals[1]) .. " -> " .. string.format("%02X", memory.readbyte(vals[1])) .. "; "
+ end
+
+ file = io.open(utils.RootPath() .. "/tmp/test-results.txt", "a")
+ io.output(file)
+
+ if expected == got then
+ io.write("-> Test '".. label .."': OK\n")
+ else
+ io.write("\n-> Test '".. label .."': FAIL\n")
+ io.write("Expected: ".. expected .. "\n")
+ io.write("Got: ".. got .. "\n")
+ io.write("\n")
+ end
+ io.close(file)
+ end)
+end
+
+function utils.StartRun(title)
+ file = io.open(utils.RootPath() .. "/tmp/test-results.txt", "a")
+ io.output(file)
+ io.write("\n== " .. title .. " ==\n")
+ io.close(file)
+end
+
+-- Ends the given test run. That is, it will exit from the emulator so we can
+-- turn back to the runner.
+function utils.EndRun()
+ -- I'm not entirely sure why this is needed, but if we don't advance for
+ -- several frames fceux won't exit. Thus, let's advance for some frames and
+ -- then quit.
+ for i = 0, 10, 1 do
+ emu.frameadvance();
+ end
+
+ emu.exit()
+end
+
+return utils