Merge remote-tracking branch 'upstream/master' into new-objects

This commit is contained in:
Corwin 2023-04-23 20:44:46 +01:00
commit 3ec9bd2610
No known key found for this signature in database
15 changed files with 255 additions and 178 deletions

View file

@ -18,6 +18,11 @@ jobs:
steps:
- name: Install build tools
run: sudo apt-get update && sudo apt-get install build-essential binutils-arm-none-eabi libelf-dev zip -y
- name: Install Miri
run: |
rustup toolchain install nightly --component miri
rustup override set nightly
cargo miri setup
- uses: actions/checkout@v3
- name: Cache
uses: actions/cache@v3

View file

@ -41,6 +41,9 @@
},
{
"path": "../examples/combo"
},
{
"path": "../agb-hashmap"
}
]
}

View file

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Importing background tiles has been improved. You no longer need to use `include_gfx!` with the toml file. Instead, use `include_background_gfx`. See the documentation for usage.
- The hashmap implementation is now it its own crate, `agb-hashmap`. There is no change in API, but you can now use this for interop between non-agb code and agb code
- Moved the existing object API to be the OamManaged API. The old names persist with deprecated notices on them.
## [0.14.0] - 2023/04/11

View file

@ -66,6 +66,8 @@ to just write games for the Game Boy Advance using this library:
* Install with `cargo install just`
* [mdbook](https://rust-lang.github.io/mdBook/index.html)
* Install with `cargo install mdbook`
* [miri](https://github.com/rust-lang/miri)
* Some of the unsafe code is tested using miri, install with `rustup component add miri`
With all of this installed, you should be able to run a full build of agb using by running
```sh
@ -85,6 +87,8 @@ for performant decimals.
`agb-sound-converter` - a crate which converts wav files into a format supported by the game boy advance
`agb-hashmap` - an no_std hashmap implementation tuned for use on the game boy advance
`agb` - the main library code
`agb/examples` - basic examples often targeting 1 feature, you can run these using `just run-example <example-name>`

13
agb-hashmap/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "agb_hashmap"
version = "0.14.0"
edition = "2021"
license = "MPL-2.0"
description = "A simple no_std hashmap implementation intended for use in the `agb` library"
repository = "https://github.com/agbrs/agb"
[dependencies]
rustc-hash = { version = "1", default-features = false }
[dev-dependencies]
rand = { version = "0.8", default-features = false, features = ["small_rng"] }

View file

@ -1,9 +1,26 @@
#![deny(missing_docs)]
//! A lot of the documentation for this module was copied straight out of the rust
//! standard library. The implementation however is not.
#![no_std]
#![feature(allocator_api)]
#![deny(clippy::all)]
#![deny(clippy::must_use_candidate)]
#![deny(missing_docs)]
#![deny(clippy::trivially_copy_pass_by_ref)]
#![deny(clippy::semicolon_if_nothing_returned)]
#![deny(clippy::map_unwrap_or)]
#![deny(clippy::needless_pass_by_value)]
#![deny(clippy::redundant_closure_for_method_calls)]
#![deny(clippy::cloned_instead_of_copied)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::private_intra_doc_links)]
#![deny(rustdoc::invalid_html_tags)]
extern crate alloc;
use alloc::{alloc::Global, vec::Vec};
use core::{
alloc::Allocator,
borrow::Borrow,
hash::{BuildHasher, BuildHasherDefault, Hash, Hasher},
iter::FromIterator,
mem::{self, MaybeUninit},
@ -83,6 +100,38 @@ type HashType = u32;
///
/// [`Eq`]: https://doc.rust-lang.org/core/cmp/trait.Eq.html
/// [`Hash`]: https://doc.rust-lang.org/core/hash/trait.Hash.html
///
/// # Example
/// ```
/// use agb_hashmap::HashMap;
///
/// // Type inference lets you omit the type signature (which would be HashMap<String, String> in this example)
/// let mut game_reviews = HashMap::new();
///
/// // Review some games
/// game_reviews.insert(
/// "Pokemon Emerald".to_string(),
/// "Best post-game battle experience of any generation.".to_string(),
/// );
/// game_reviews.insert(
/// "Golden Sun".to_string(),
/// "Some of the best music on the console".to_string(),
/// );
/// game_reviews.insert(
/// "Super Dodge Ball Advance".to_string(),
/// "Really great launch title".to_string(),
/// );
///
/// // Check for a specific entry
/// if !game_reviews.contains_key("Legend of Zelda: The Minish Cap") {
/// println!("We've got {} reviews, but The Minish Cap ain't one", game_reviews.len());
/// }
///
/// // Iterate over everything
/// for (game, review) in &game_reviews {
/// println!("{game}: \"{review}\"");
/// }
/// ```
pub struct HashMap<K, V, ALLOCATOR: Allocator = Global> {
nodes: NodeStorage<K, V, ALLOCATOR>,
@ -283,13 +332,21 @@ where
}
/// Returns `true` if the map contains a value for the specified key.
pub fn contains_key(&self, k: &K) -> bool {
pub fn contains_key<Q>(&self, k: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let hash = self.hash(k);
self.nodes.location(k, hash).is_some()
}
/// Returns the key-value pair corresponding to the supplied key
pub fn get_key_value(&self, key: &K) -> Option<(&K, &V)> {
pub fn get_key_value<Q>(&self, key: &Q) -> Option<(&K, &V)>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let hash = self.hash(key);
self.nodes
@ -299,13 +356,45 @@ where
/// Returns a reference to the value corresponding to the key. Returns [`None`] if there is
/// no element in the map with the given key.
pub fn get(&self, key: &K) -> Option<&V> {
///
/// # Example
/// ```
/// use agb_hashmap::HashMap;
///
/// let mut map = HashMap::new();
/// map.insert("a".to_string(), "A");
/// assert_eq!(map.get("a"), Some(&"A"));
/// assert_eq!(map.get("b"), None);
/// ```
pub fn get<Q>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.get_key_value(key).map(|(_, v)| v)
}
/// Returns a mutable reference to the value corresponding to the key. Return [`None`] if
/// there is no element in the map with the given key.
pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
///
/// # Example
/// ```
/// use agb_hashmap::HashMap;
///
/// let mut map = HashMap::new();
/// map.insert("a".to_string(), "A");
///
/// if let Some(x) = map.get_mut("a") {
/// *x = "b";
/// }
///
/// assert_eq!(map["a"], "b");
/// ```
pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let hash = self.hash(key);
if let Some(location) = self.nodes.location(key, hash) {
@ -317,7 +406,21 @@ where
/// Removes the given key from the map. Returns the current value if it existed, or [`None`]
/// if it did not.
pub fn remove(&mut self, key: &K) -> Option<V> {
///
/// # Example
/// ```
/// use agb_hashmap::HashMap;
///
/// let mut map = HashMap::new();
/// map.insert(1, "a");
/// assert_eq!(map.remove(&1), Some("a"));
/// assert_eq!(map.remove(&1), None);
/// ```
pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let hash = self.hash(key);
self.nodes
@ -330,7 +433,11 @@ impl<K, V, ALLOCATOR: ClonableAllocator> HashMap<K, V, ALLOCATOR>
where
K: Hash,
{
fn hash(&self, key: &K) -> HashType {
fn hash<Q>(&self, key: &Q) -> HashType
where
K: Borrow<Q>,
Q: Hash + ?Sized,
{
let mut hasher = self.hasher.build_hasher();
key.hash(&mut hasher);
hasher.finish() as HashType
@ -657,28 +764,18 @@ where
}
}
impl<K, V, ALLOCATOR: ClonableAllocator> Index<&K> for HashMap<K, V, ALLOCATOR>
impl<K, V, Q, ALLOCATOR: ClonableAllocator> Index<&Q> for HashMap<K, V, ALLOCATOR>
where
K: Eq + Hash,
K: Eq + Hash + Borrow<Q>,
Q: Eq + Hash + ?Sized,
{
type Output = V;
fn index(&self, key: &K) -> &V {
fn index(&self, key: &Q) -> &V {
self.get(key).expect("no entry found for key")
}
}
impl<K, V, ALLOCATOR: ClonableAllocator> Index<K> for HashMap<K, V, ALLOCATOR>
where
K: Eq + Hash,
{
type Output = V;
fn index(&self, key: K) -> &V {
self.get(&key).expect("no entry found for key")
}
}
const fn number_before_resize(capacity: usize) -> usize {
capacity * 85 / 100
}
@ -812,9 +909,10 @@ impl<K, V, ALLOCATOR: ClonableAllocator> NodeStorage<K, V, ALLOCATOR> {
}
}
fn location(&self, key: &K, hash: HashType) -> Option<usize>
fn location<Q>(&self, key: &Q, hash: HashType) -> Option<usize>
where
K: Eq,
K: Borrow<Q>,
Q: Eq + ?Sized,
{
for distance_to_initial_bucket in 0..(self.max_distance_to_initial_bucket + 1) {
let location = fast_mod(
@ -824,7 +922,7 @@ impl<K, V, ALLOCATOR: ClonableAllocator> NodeStorage<K, V, ALLOCATOR> {
let node = &self.nodes[location];
if let Some(node_key_ref) = node.key_ref() {
if node_key_ref == key {
if node_key_ref.borrow() == key {
return Some(location);
}
} else {
@ -993,10 +1091,9 @@ mod test {
use core::cell::RefCell;
use super::*;
use crate::{rng::RandomNumberGenerator, Gba};
#[test_case]
fn can_store_and_retrieve_8_elements(_gba: &mut Gba) {
#[test]
fn can_store_and_retrieve_8_elements() {
let mut map = HashMap::new();
for i in 0..8 {
@ -1008,8 +1105,8 @@ mod test {
}
}
#[test_case]
fn can_get_the_length(_gba: &mut Gba) {
#[test]
fn can_get_the_length() {
let mut map = HashMap::new();
for i in 0..8 {
@ -1019,8 +1116,8 @@ mod test {
assert_eq!(map.len(), 4);
}
#[test_case]
fn returns_none_if_element_does_not_exist(_gba: &mut Gba) {
#[test]
fn returns_none_if_element_does_not_exist() {
let mut map = HashMap::new();
for i in 0..8 {
@ -1030,8 +1127,8 @@ mod test {
assert_eq!(map.get(&12), None);
}
#[test_case]
fn can_delete_entries(_gba: &mut Gba) {
#[test]
fn can_delete_entries() {
let mut map = HashMap::new();
for i in 0..8 {
@ -1047,8 +1144,8 @@ mod test {
assert_eq!(map.get(&7), Some(&1));
}
#[test_case]
fn can_iterate_through_all_entries(_gba: &mut Gba) {
#[test]
fn can_iterate_through_all_entries() {
let mut map = HashMap::new();
for i in 0..8 {
@ -1067,8 +1164,8 @@ mod test {
assert_eq!(max_found, 7);
}
#[test_case]
fn can_insert_more_than_initial_capacity(_gba: &mut Gba) {
#[test]
fn can_insert_more_than_initial_capacity() {
let mut map = HashMap::new();
for i in 0..65 {
@ -1086,6 +1183,7 @@ mod test {
}
impl NoisyDrop {
#[cfg(not(miri))]
fn new(i: i32) -> Self {
Self { i, dropped: false }
}
@ -1115,17 +1213,33 @@ mod test {
}
}
#[test_case]
fn extreme_case(_gba: &mut Gba) {
trait RngNextI32 {
fn next_i32(&mut self) -> i32;
}
impl<T> RngNextI32 for T
where
T: rand::RngCore,
{
fn next_i32(&mut self) -> i32 {
self.next_u32() as i32
}
}
#[cfg(not(miri))] // takes way too long to run under miri
#[test]
fn extreme_case() {
use rand::SeedableRng;
let mut map = HashMap::new();
let mut rng = RandomNumberGenerator::new();
let mut rng = rand::rngs::SmallRng::seed_from_u64(20);
let mut answers: [Option<i32>; 128] = [None; 128];
for _ in 0..5_000 {
let command = rng.gen().rem_euclid(2);
let key = rng.gen().rem_euclid(answers.len() as i32);
let value = rng.gen();
let command = rng.next_i32().rem_euclid(2);
let key = rng.next_i32().rem_euclid(answers.len() as i32);
let value = rng.next_i32();
match command {
0 => {
@ -1212,8 +1326,8 @@ mod test {
}
}
#[test_case]
fn correctly_drops_on_remove_and_overall_drop(_gba: &mut Gba) {
#[test]
fn correctly_drops_on_remove_and_overall_drop() {
let drop_registry = DropRegistry::new();
let droppable1 = drop_registry.new_droppable();
@ -1239,8 +1353,8 @@ mod test {
drop_registry.assert_dropped_once(id2);
}
#[test_case]
fn correctly_drop_on_override(_gba: &mut Gba) {
#[test]
fn correctly_drop_on_override() {
let drop_registry = DropRegistry::new();
let droppable1 = drop_registry.new_droppable();
@ -1263,8 +1377,8 @@ mod test {
drop_registry.assert_dropped_once(id2);
}
#[test_case]
fn correctly_drops_key_on_override(_gba: &mut Gba) {
#[test]
fn correctly_drops_key_on_override() {
let drop_registry = DropRegistry::new();
let droppable1 = drop_registry.new_droppable();
@ -1285,8 +1399,8 @@ mod test {
drop_registry.assert_dropped_n_times(id1, 2);
}
#[test_case]
fn test_retain(_gba: &mut Gba) {
#[test]
fn test_retain() {
let mut map = HashMap::new();
for i in 0..100 {
@ -1301,8 +1415,8 @@ mod test {
assert_eq!(map.iter().count(), 50); // force full iteration
}
#[test_case]
fn test_size_hint_iter(_gba: &mut Gba) {
#[test]
fn test_size_hint_iter() {
let mut map = HashMap::new();
for i in 0..100 {
@ -1317,8 +1431,8 @@ mod test {
assert_eq!(iter.size_hint(), (99, Some(99)));
}
#[test_case]
fn test_size_hint_into_iter(_gba: &mut Gba) {
#[test]
fn test_size_hint_into_iter() {
let mut map = HashMap::new();
for i in 0..100 {
@ -1336,13 +1450,10 @@ mod test {
// Following test cases copied from the rust source
// https://github.com/rust-lang/rust/blob/master/library/std/src/collections/hash/map/tests.rs
mod rust_std_tests {
use crate::{
hash_map::{Entry::*, HashMap},
Gba,
};
use crate::{Entry::*, HashMap};
#[test_case]
fn test_entry(_gba: &mut Gba) {
#[test]
fn test_entry() {
let xs = [(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)];
let mut map: HashMap<_, _> = xs.iter().copied().collect();
@ -1391,8 +1502,8 @@ mod test {
assert_eq!(map.len(), 6);
}
#[test_case]
fn test_occupied_entry_key(_gba: &mut Gba) {
#[test]
fn test_occupied_entry_key() {
let mut a = HashMap::new();
let key = "hello there";
let value = "value goes here";
@ -1409,8 +1520,8 @@ mod test {
assert_eq!(a[key], value);
}
#[test_case]
fn test_vacant_entry_key(_gba: &mut Gba) {
#[test]
fn test_vacant_entry_key() {
let mut a = HashMap::new();
let key = "hello there";
let value = "value goes here";
@ -1427,8 +1538,8 @@ mod test {
assert_eq!(a[key], value);
}
#[test_case]
fn test_index(_gba: &mut Gba) {
#[test]
fn test_index() {
let mut map = HashMap::new();
map.insert(1, 2);
@ -1438,111 +1549,4 @@ mod test {
assert_eq!(map[&2], 1);
}
}
mod rust_std_tests_custom_allocator {
use crate::{
hash_map::{Entry::*, HashMap},
Gba, InternalAllocator,
};
#[test_case]
fn test_entry(_gba: &mut Gba) {
let xs = [(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)];
let mut map = HashMap::new_in(InternalAllocator);
for (k, v) in xs {
map.insert(k, v);
}
// Existing key (insert)
match map.entry(1) {
Vacant(_) => unreachable!(),
Occupied(mut view) => {
assert_eq!(view.get(), &10);
assert_eq!(view.insert(100), 10);
}
}
assert_eq!(map.get(&1).unwrap(), &100);
assert_eq!(map.len(), 6);
// Existing key (update)
match map.entry(2) {
Vacant(_) => unreachable!(),
Occupied(mut view) => {
let v = view.get_mut();
let new_v = (*v) * 10;
*v = new_v;
}
}
assert_eq!(map.get(&2).unwrap(), &200);
assert_eq!(map.len(), 6);
// Existing key (take)
match map.entry(3) {
Vacant(_) => unreachable!(),
Occupied(view) => {
assert_eq!(view.remove(), 30);
}
}
assert_eq!(map.get(&3), None);
assert_eq!(map.len(), 5);
// Inexistent key (insert)
match map.entry(10) {
Occupied(_) => unreachable!(),
Vacant(view) => {
assert_eq!(*view.insert(1000), 1000);
}
}
assert_eq!(map.get(&10).unwrap(), &1000);
assert_eq!(map.len(), 6);
}
#[test_case]
fn test_occupied_entry_key(_gba: &mut Gba) {
let mut a = HashMap::new_in(InternalAllocator);
let key = "hello there";
let value = "value goes here";
assert!(a.is_empty());
a.insert(key, value);
assert_eq!(a.len(), 1);
assert_eq!(a[key], value);
match a.entry(key) {
Vacant(_) => panic!(),
Occupied(e) => assert_eq!(key, *e.key()),
}
assert_eq!(a.len(), 1);
assert_eq!(a[key], value);
}
#[test_case]
fn test_vacant_entry_key(_gba: &mut Gba) {
let mut a = HashMap::new_in(InternalAllocator);
let key = "hello there";
let value = "value goes here";
assert!(a.is_empty());
match a.entry(key) {
Occupied(_) => panic!(),
Vacant(e) => {
assert_eq!(key, *e.key());
e.insert(value);
}
}
assert_eq!(a.len(), 1);
assert_eq!(a[key], value);
}
#[test_case]
fn test_index(_gba: &mut Gba) {
let mut map = HashMap::new_in(InternalAllocator);
map.insert(1, 2);
map.insert(2, 1);
map.insert(3, 4);
assert_eq!(map[&2], 1);
}
}
}

View file

@ -27,6 +27,7 @@ agb_image_converter = { version = "0.14.0", path = "../agb-image-converter" }
agb_sound_converter = { version = "0.14.0", path = "../agb-sound-converter" }
agb_macros = { version = "0.14.0", path = "../agb-macros" }
agb_fixnum = { version = "0.14.0", path = "../agb-fixnum" }
agb_hashmap = { version = "0.14.0", path = "../agb-hashmap" }
bare-metal = "1"
modular-bitfield = "0.11"
rustc-hash = { version = "1", default-features = false }

View file

@ -151,7 +151,7 @@ pub mod mgba;
#[doc(inline)]
pub use agb_fixnum as fixnum;
/// Contains an implementation of a hashmap which suits the gameboy advance's hardware.
pub mod hash_map;
pub use agb_hashmap as hash_map;
/// Simple random number generator
pub mod rng;
pub mod save;

View file

@ -19,6 +19,7 @@ name = "agb"
version = "0.14.0"
dependencies = [
"agb_fixnum",
"agb_hashmap",
"agb_image_converter",
"agb_macros",
"agb_sound_converter",
@ -35,6 +36,13 @@ dependencies = [
"agb_macros",
]
[[package]]
name = "agb_hashmap"
version = "0.14.0"
dependencies = [
"rustc-hash",
]
[[package]]
name = "agb_image_converter"
version = "0.14.0"

View file

@ -19,6 +19,7 @@ name = "agb"
version = "0.14.0"
dependencies = [
"agb_fixnum",
"agb_hashmap",
"agb_image_converter",
"agb_macros",
"agb_sound_converter",
@ -35,6 +36,13 @@ dependencies = [
"agb_macros",
]
[[package]]
name = "agb_hashmap"
version = "0.14.0"
dependencies = [
"rustc-hash",
]
[[package]]
name = "agb_image_converter"
version = "0.14.0"

View file

@ -19,6 +19,7 @@ name = "agb"
version = "0.14.0"
dependencies = [
"agb_fixnum",
"agb_hashmap",
"agb_image_converter",
"agb_macros",
"agb_sound_converter",
@ -35,6 +36,13 @@ dependencies = [
"agb_macros",
]
[[package]]
name = "agb_hashmap"
version = "0.14.0"
dependencies = [
"rustc-hash",
]
[[package]]
name = "agb_image_converter"
version = "0.14.0"

View file

@ -19,6 +19,7 @@ name = "agb"
version = "0.14.0"
dependencies = [
"agb_fixnum",
"agb_hashmap",
"agb_image_converter",
"agb_macros",
"agb_sound_converter",
@ -35,6 +36,13 @@ dependencies = [
"agb_macros",
]
[[package]]
name = "agb_hashmap"
version = "0.14.0"
dependencies = [
"rustc-hash",
]
[[package]]
name = "agb_image_converter"
version = "0.14.0"

View file

@ -19,6 +19,7 @@ name = "agb"
version = "0.14.0"
dependencies = [
"agb_fixnum",
"agb_hashmap",
"agb_image_converter",
"agb_macros",
"agb_sound_converter",
@ -35,6 +36,13 @@ dependencies = [
"agb_macros",
]
[[package]]
name = "agb_hashmap"
version = "0.14.0"
dependencies = [
"rustc-hash",
]
[[package]]
name = "agb_image_converter"
version = "0.14.0"

View file

@ -14,6 +14,7 @@ clippy:
test:
just _test-debug agb
just _test-debug agb-fixnum
just _test-debug agb-hashmap
just _test-debug-arm agb
just _test-debug tools
@ -27,6 +28,7 @@ doctest-agb:
check-docs:
(cd agb && cargo doc --target=thumbv6m-none-eabi --no-deps)
just _build_docs agb-fixnum
just _build_docs agb-hashmap
_build_docs crate:
(cd "{{crate}}" && cargo doc --no-deps)
@ -59,7 +61,7 @@ check-linker-script-consistency:
find -type f -name gba.ld -print0 | xargs -0 -n1 cmp -- agb/gba.ld
find -type f -name gba_mb.ld -print0 | xargs -0 -n1 cmp -- agb/gba_mb.ld
ci: check-linker-script-consistency build-debug clippy fmt-check test build-release test-release doctest-agb build-roms build-book check-docs
ci: check-linker-script-consistency build-debug clippy fmt-check test miri build-release test-release doctest-agb build-roms build-book check-docs
build-roms:
just _build-rom "examples/the-purple-night" "PURPLENIGHT"
@ -85,6 +87,9 @@ publish *args: (_run-tool "publish" args)
release +args: (_run-tool "release" args)
miri:
(cd agb-hashmap && cargo miri test)
_run-tool +tool:
(cd tools && cargo build)
"$CARGO_TARGET_DIR/debug/tools" {{tool}}

View file

@ -170,7 +170,8 @@ mod test {
"agb-image-converter",
"agb-sound-converter",
"agb-macros",
"agb-fixnum"
"agb-fixnum",
"agb-hashmap",
]
);
Ok(())