From 12be8fe070d6c3b0ef04c744ba300063f30791cf Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 11 Apr 2023 21:54:47 +0200 Subject: [PATCH] Let dictionaries respect insertion order --- docs/src/reference/types.md | 16 +++++++--------- src/eval/dict.rs | 28 ++++++++++++++++++++-------- src/eval/mod.rs | 4 ++-- src/eval/value.rs | 2 +- tests/ref/compiler/for.png | Bin 3352 -> 3342 bytes tests/typ/compiler/dict.typ | 4 ++-- tests/typ/compiler/for.typ | 2 +- 7 files changed, 33 insertions(+), 23 deletions(-) diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md index 184da1377..7183bac4e 100644 --- a/docs/src/reference/types.md +++ b/docs/src/reference/types.md @@ -674,7 +674,9 @@ Return a new array with the same items, but sorted. A map from string keys to values. You can construct a dictionary by enclosing comma-separated `key: value` pairs -in parentheses. The values do not have to be of the same type. +in parentheses. The values do not have to be of the same type. Since empty +parentheses already yield an empty array, you have to use the special `(:)` +syntax to create an empty dictionary. A dictionary is conceptually similar to an array, but it is indexed by strings instead of integers. You can access and create dictionary entries with the @@ -685,12 +687,8 @@ the value. Dictionaries can be added with the `+` operator and To check whether a key is present in the dictionary, use the `in` keyword. You can iterate over the pairs in a dictionary using a -[for loop]($scripting/#loops). -Dictionaries are always ordered by key. - -Since empty parentheses already yield an empty array, you have to use the -special `(:)` syntax to create an empty dictionary. - +[for loop]($scripting/#loops). This will iterate in the order the pairs were +inserted / declared. ## Example ```example @@ -735,12 +733,12 @@ If the dictionary already contains this key, the value is updated. The value of the pair that should be inserted. ### keys() -Returns the keys of the dictionary as an array in sorted order. +Returns the keys of the dictionary as an array in insertion order. - returns: array ### values() -Returns the values of the dictionary as an array in key-order. +Returns the values of the dictionary as an array in insertion order. - returns: array diff --git a/src/eval/dict.rs b/src/eval/dict.rs index 4333a55eb..b137f03c7 100644 --- a/src/eval/dict.rs +++ b/src/eval/dict.rs @@ -1,5 +1,5 @@ -use std::collections::BTreeMap; use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign}; use std::sync::Arc; @@ -16,7 +16,7 @@ use crate::util::{pretty_array_like, separated_list, ArcExt}; macro_rules! __dict { ($($key:expr => $value:expr),* $(,)?) => {{ #[allow(unused_mut)] - let mut map = std::collections::BTreeMap::new(); + let mut map = $crate::eval::IndexMap::new(); $(map.insert($key.into(), $value.into());)* $crate::eval::Dict::from_map(map) }}; @@ -25,9 +25,12 @@ macro_rules! __dict { #[doc(inline)] pub use crate::__dict as dict; +#[doc(inline)] +pub use indexmap::IndexMap; + /// A reference-counted dictionary with value semantics. -#[derive(Default, Clone, PartialEq, Hash)] -pub struct Dict(Arc>); +#[derive(Default, Clone, PartialEq)] +pub struct Dict(Arc>); impl Dict { /// Create a new, empty dictionary. @@ -36,7 +39,7 @@ impl Dict { } /// Create a new dictionary from a mapping of strings to values. - pub fn from_map(map: BTreeMap) -> Self { + pub fn from_map(map: IndexMap) -> Self { Self(Arc::new(map)) } @@ -116,7 +119,7 @@ impl Dict { } /// Iterate over pairs of references to the contained keys and values. - pub fn iter(&self) -> std::collections::btree_map::Iter { + pub fn iter(&self) -> indexmap::map::Iter { self.0.iter() } @@ -171,6 +174,15 @@ impl AddAssign for Dict { } } +impl Hash for Dict { + fn hash(&self, state: &mut H) { + state.write_usize(self.0.len()); + for item in self { + item.hash(state); + } + } +} + impl Extend<(Str, Value)> for Dict { fn extend>(&mut self, iter: T) { Arc::make_mut(&mut self.0).extend(iter); @@ -185,7 +197,7 @@ impl FromIterator<(Str, Value)> for Dict { impl IntoIterator for Dict { type Item = (Str, Value); - type IntoIter = std::collections::btree_map::IntoIter; + type IntoIter = indexmap::map::IntoIter; fn into_iter(self) -> Self::IntoIter { Arc::take(self.0).into_iter() @@ -194,7 +206,7 @@ impl IntoIterator for Dict { impl<'a> IntoIterator for &'a Dict { type Item = (&'a Str, &'a Value); - type IntoIter = std::collections::btree_map::Iter<'a, Str, Value>; + type IntoIter = indexmap::map::Iter<'a, Str, Value>; fn into_iter(self) -> Self::IntoIter { self.iter() diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 55d2a7341..5a4504816 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -37,7 +37,7 @@ pub use self::value::*; pub(crate) use self::methods::methods_on; -use std::collections::{BTreeMap, HashSet}; +use std::collections::HashSet; use std::mem; use std::path::{Path, PathBuf}; @@ -870,7 +870,7 @@ impl Eval for ast::Dict { type Output = Dict; fn eval(&self, vm: &mut Vm) -> SourceResult { - let mut map = BTreeMap::new(); + let mut map = indexmap::IndexMap::new(); for item in self.items() { match item { diff --git a/src/eval/value.rs b/src/eval/value.rs index b8a51c701..3fd9ed422 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -444,6 +444,6 @@ mod tests { test(array![1, 2], "(1, 2)"); test(dict![], "(:)"); test(dict!["one" => 1], "(one: 1)"); - test(dict!["two" => false, "one" => 1], "(one: 1, two: false)"); + test(dict!["two" => false, "one" => 1], "(two: false, one: 1)"); } } diff --git a/tests/ref/compiler/for.png b/tests/ref/compiler/for.png index 48fde203a5a2c4762fb9baf99c35eb14adcd86fa..5608248f828efa5ef8c09608a3ae9a8d3d5b7454 100644 GIT binary patch literal 3342 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 R`@p|N2d(~4tpsHm`Y%IJO)UTb literal 3352 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 TBJ6?J0DsWaGSDoCT7~}uMATTI diff --git a/tests/typ/compiler/dict.typ b/tests/typ/compiler/dict.typ index c8dd086fe..fb0a59a36 100644 --- a/tests/typ/compiler/dict.typ +++ b/tests/typ/compiler/dict.typ @@ -48,8 +48,8 @@ #let dict = (a: 3, c: 2, b: 1) #test("c" in dict, true) #test(dict.len(), 3) -#test(dict.values(), (3, 1, 2)) -#test(dict.pairs().map(p => p.first() + str(p.last())).join(), "a3b1c2") +#test(dict.values(), (3, 2, 1)) +#test(dict.pairs().map(p => p.first() + str(p.last())).join(), "a3c2b1") #dict.remove("c") #test("c" in dict, false) diff --git a/tests/typ/compiler/for.typ b/tests/typ/compiler/for.typ index 4a3aefb41..f525215ff 100644 --- a/tests/typ/compiler/for.typ +++ b/tests/typ/compiler/for.typ @@ -7,7 +7,7 @@ // Empty array. #for x in () [Nope] -// Dictionary is not traversed in insertion order. +// Dictionary is traversed in insertion order. // Should output `Age: 2. Name: Typst.`. #for (k, v) in (Name: "Typst", Age: 2) [ #k: #v.