lysp: add hashtable support

This commit is contained in:
2026-06-02 17:21:47 +03:00
parent befdf63c7c
commit fd8e1df696
12 changed files with 597 additions and 11 deletions
+52
View File
@@ -0,0 +1,52 @@
(setq h (hash/new
'(key . value)
'(1 . 2)
'("string" . 3)
'(3 . 4)))
(assert (= 'value (hash/get h 'key)))
(assert (= 2 (hash/get h 1)))
(assert (= 100 (hash/get h 0 100)))
(assert (= NIL (hash/get h 0)))
(hash/map! (lambda (_ v) v) h)
(assert (= 'value (hash/get h 'key)))
(assert (= 2 (hash/get h 1)))
(assert (= 100 (hash/get h 0 100)))
(assert (= NIL (hash/get h 0)))
(hash/remove! h 'key)
(assert (= NIL (hash/get h 'key)))
(hash/map! (lambda (_ v) (+ v 100)) h)
(assert (= 102 (hash/get h 1)))
(assert (= 103 (hash/get h "string")))
(assert (= 100 (hash/get h 0 100)))
(assert (= NIL (hash/get h 0)))
(setq hl (hash->list h))
(assert (= '(3 . 104) (find (lambda (a) (= (car a) 3)) hl)))
(assert (= '("string" . 103) (find (lambda (a) (= (car a) "string")) hl)))
(assert (= '(1 . 102) (find (lambda (a) (= (car a) 1)) hl)))
(setq sum-1 0)
(hash/for-each (lambda (_ v) (setq sum-1 (+ sum-1 v))) h)
(setq sum-2 (hash/fold 0 (lambda (a _ v) (+ a v)) h))
(assert (= sum-1 sum-2 309))
(hash/filter! (lambda (k _) (/= k "string")) h)
;; Hash equality
(assert (= h (hash/new '(1 . 102) '(3 . 104))))
(assert (=
(hash/new '(1 . 2) '(2 . 3) '("string" . "value"))
(list->hash '((2 . 3) ("string" . "value") (1 . 2)))
))
;; hash->list->hash idempotence
(setq h (hash/new))
(let (i 0)
(while (< i 10000)
(hash/put! h (+ "key" i) (+ "value" i))
(setq i (+ i 1))
))
(assert (= h (list->hash (hash->list h))))
@@ -66,6 +66,7 @@ impl Expression {
fn parse_inner(value: &Value) -> Rc<Self> {
match value {
Value::Vector(vector) => Rc::new(Self::Vector(vector.clone())),
Value::HashTable(_) => todo!(),
Value::String(value) => Rc::new(Self::StringLiteral(value.clone())),
Value::Quasi(_value) => todo!("{value}"),
Value::Unquote(_value) => todo!("Unquote {_value}"),
+2
View File
@@ -97,6 +97,8 @@ pub enum MachineError {
Read(ReadError),
#[error("compile error: {0}")]
Compile(#[from] CompileError),
#[error("value cannot be used as a hashmap key: {0}")]
InvalidHashTableKey(Value),
}
impl MachineError {
+1
View File
@@ -40,6 +40,7 @@ impl MacroExpand for Value {
| Self::NativeFunction(_)
| Self::NativeValue(_)
| Self::Vector(_)
| Self::HashTable(_)
| Self::UnquoteSplice(_)
| Self::Unquote(_) => Ok(self.clone()),
// | Self::NativeFunction(_) => Ok(self.clone()),
@@ -6,7 +6,7 @@ use crate::{
Value,
env::Environment,
value::{
ConsCell,
ConsCell, HashTable, HashTableData,
convert::{AnyFunction, TryFromValue},
},
},
@@ -154,4 +154,183 @@ pub fn load(env: &Rc<Environment>) {
Ok(Value::Nil)
},
);
// Hash table
env.defun_native(
"hash/new",
"Creates a hash table from the list of pairs",
|_, _, args| {
let mut hash = HashTableData::new(16);
for arg in args {
let Value::Cons(cons) = arg else {
return Err(MachineError::ValueConversion(ValueConversionError {
expected: "a pair".into(),
got: arg.clone(),
}));
};
let ConsCell(car, cdr) = cons.as_ref();
hash.insert(car.clone(), cdr.clone())?;
}
let hash = HashTable::from(hash);
Ok(hash.into())
},
);
env.defun_native(
"hash/length",
"Returns the number of associations in the hashtable",
|_, _, args| {
let [table] = args else {
return Err(MachineError::InvalidArgumentCount);
};
let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
let len = table.borrow().len();
Ok(len.into())
},
);
env.defun_native(
"hash->list",
"Converts a hashtable into a list of pairs",
|_, _, args| {
let [table] = args else {
return Err(MachineError::InvalidArgumentCount);
};
let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
let mut list = Value::Nil;
for (key, value) in table.borrow().iter() {
list = key.clone().cons(value.clone()).cons(list);
}
Ok(list)
},
);
env.defun_native(
"list->hash",
"Converts a list of pairs into a hashtable",
|_, _, args| {
let [pairs] = args else {
return Err(MachineError::InvalidArgumentCount);
};
let mut hash = HashTableData::new(16);
let pair_iter =
pairs.proper_iter(MachineError::ValueConversion(ValueConversionError {
expected: "a list of pairs".into(),
got: pairs.clone(),
}));
for pair in pair_iter {
let pair = pair?;
let Value::Cons(cons) = pair else {
return Err(MachineError::ValueConversion(ValueConversionError {
expected: "a pair".into(),
got: pair.clone(),
}));
};
let ConsCell(car, cdr) = cons.as_ref();
hash.insert(car.clone(), cdr.clone())?;
}
let hash = HashTable::from(hash);
Ok(hash.into())
},
);
env.defun_native(
"hash/put!",
"Inserts an association into the hashtable",
|_, _, args| {
let [table, key, value] = args else {
return Err(MachineError::InvalidArgumentCount);
};
let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
let value = table.borrow_mut().insert(key.clone(), value.clone())?;
Ok(value)
},
);
env.defun_native(
"hash/remove!",
"Removes an association from the hashtable",
|_, _, args| {
let [table, key] = args else {
return Err(MachineError::InvalidArgumentCount);
};
let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
let value = table.borrow_mut().remove(key).unwrap_or(Value::Nil);
Ok(value)
},
);
env.defun_native(
"hash/for-each",
"Applies a (k, v) -> void function to all associations in the hashtable",
|vm, env, args| {
let [function, table] = args else {
return Err(MachineError::InvalidArgumentCount);
};
let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
let function = AnyFunction::try_from_value(function)?;
for (key, value) in table.borrow().iter() {
function.invoke(vm, env, &[key.clone(), value.clone()])?;
}
Ok(Value::Nil)
},
);
env.defun_native(
"hash/fold",
"Applies a (acc, k, v) -> acc' function to all associations in the hashtable",
|vm, env, args| {
let [acc, function, table] = args else {
return Err(MachineError::InvalidArgumentCount);
};
let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
let function = AnyFunction::try_from_value(function)?;
let mut acc = acc.clone();
for (key, value) in table.borrow().iter() {
acc = function.invoke(vm, env, &[acc, key.clone(), value.clone()])?;
}
Ok(acc)
},
);
env.defun_native(
"hash/filter!",
"Removes associations not matching the predicate from the hashtable",
|vm, env, args| {
let [predicate, table] = args else {
return Err(MachineError::InvalidArgumentCount);
};
let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
let predicate = AnyFunction::try_from_value(predicate)?;
table.borrow_mut().retain_invoke(vm, env, &predicate)?;
Ok(Value::Nil)
},
);
env.defun_native(
"hash/map!",
"Applies a (k, v) -> v' transform on the hashtable",
|vm, env, args| {
let [transform, table] = args else {
return Err(MachineError::InvalidArgumentCount);
};
let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
let transform = AnyFunction::try_from_value(transform)?;
{
let mut borrow = table.borrow_mut();
borrow.iter_mut().try_for_each(|(key, value)| {
let output = transform.invoke(vm, env, &[key.clone(), value.clone()])?;
*value = output;
Ok::<_, MachineError>(())
})?;
}
Ok(Value::Nil)
},
);
env.defun_native(
"hash/get",
"Retrieves a value from the hashtable associated with given key",
|_, _, args| {
let (table, key, default) = match args {
[table, key] => (table, key, &Value::Nil),
[table, key, default] => (table, key, default),
_ => return Err(MachineError::InvalidArgumentCount),
};
let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
let value = table.borrow().get(key).unwrap_or(default).clone();
Ok(value)
},
);
}
@@ -73,6 +73,7 @@ pub fn load(env: &Rc<Environment>) {
Value::UnquoteSplice(_) => "an unquote-spliced value".into(),
Value::Identifier(identifier) => format!("an identifier {:?}", identifier.as_ref()),
Value::Vector(_) => "a vector".into(),
Value::HashTable(_) => "a hash table".into(),
Value::String(_) => "a string".into(),
Value::Keyword(_) => "a keyword".into(),
Value::Closure(closure) => {
+5 -2
View File
@@ -104,8 +104,11 @@ pub(super) fn load(env: &Rc<Environment>) {
fn value_add(a: &Value, b: &Value) -> Result<Value, MachineError> {
match (a, b) {
(Value::String(a), _) => Ok(Value::String(format!("{a}{b}").into())),
(_, Value::String(b)) => Ok(Value::String(format!("{a}{b}").into())),
(Value::String(a), Value::String(b)) => {
Ok(Value::String(format!("{}{}", &**a, &**b).into()))
}
(Value::String(a), _) => Ok(Value::String(format!("{}{b}", &**a).into())),
(_, Value::String(b)) => Ok(Value::String(format!("{a}{}", &**b).into())),
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(*a + *b)),
(Value::Number(a), _) => Ok(Value::Number(*a + NumberValue::try_from_value(b)?)),
(_, Value::Number(b)) => Ok(Value::Number(NumberValue::try_from_value(a)? + *b)),
+19 -1
View File
@@ -7,7 +7,7 @@ use crate::{
env::Environment,
machine::Machine,
value::{
BooleanValue, BytecodeFunction, ClosureValue, ConsCell, IdentifierValue,
BooleanValue, BytecodeFunction, ClosureValue, ConsCell, HashTable, IdentifierValue,
NativeFunction, NativeValue, NumberValue, StringValue, Vector,
},
},
@@ -139,6 +139,24 @@ impl From<StringValue> for Value {
}
}
impl TryFromValue<'_> for Rc<HashTable> {
fn try_from_value(value: &'_ Value) -> Result<Self, ValueConversionError> {
match value {
Value::HashTable(table) => Ok(table.clone()),
_ => Err(ValueConversionError {
expected: "hash".into(),
got: value.clone(),
}),
}
}
}
impl From<HashTable> for Value {
fn from(value: HashTable) -> Self {
Value::HashTable(Rc::new(value))
}
}
impl TryFromValue<'_> for IdentifierValue {
fn try_from_value(value: &'_ Value) -> Result<Self, ValueConversionError> {
match value {
@@ -0,0 +1,234 @@
use std::{
cell::{Ref, RefCell, RefMut},
fmt,
rc::Rc,
slice,
};
use crate::{
error::MachineError,
vm::{Value, env::Environment, machine::Machine, value::convert::AnyFunction},
};
#[derive(Debug, Clone, PartialEq)]
pub struct HashTable(RefCell<HashTableData>);
#[derive(Clone)]
pub struct HashTableData {
buckets: Box<[Vec<(Value, Value)>]>,
length: usize,
}
pub struct HashTableIter<'a> {
buckets: slice::Iter<'a, Vec<(Value, Value)>>,
inner: Option<slice::Iter<'a, (Value, Value)>>,
}
pub struct HashTableIterMut<'a> {
buckets: slice::IterMut<'a, Vec<(Value, Value)>>,
inner: Option<slice::IterMut<'a, (Value, Value)>>,
}
impl HashTable {
pub fn borrow(&self) -> Ref<'_, HashTableData> {
self.0.borrow()
}
pub fn borrow_mut(&self) -> RefMut<'_, HashTableData> {
self.0.borrow_mut()
}
}
impl fmt::Display for HashTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&*self.0.borrow(), f)
}
}
impl HashTableData {
pub fn new(bucket_count: usize) -> Self {
let buckets = vec![vec![]; bucket_count].into_boxed_slice();
Self { buckets, length: 0 }
}
pub fn is_empty(&self) -> bool {
self.length == 0
}
pub fn len(&self) -> usize {
self.length
}
pub fn clear(&mut self) {
for bucket in self.buckets.iter_mut() {
bucket.clear();
}
self.length = 0;
}
// Returns the value inserted
pub fn insert(&mut self, key: Value, value: Value) -> Result<Value, MachineError> {
let hash = key
.hash()
.ok_or_else(|| MachineError::InvalidHashTableKey(key.clone()))?;
let bucket_index = (hash % self.buckets.len() as u64) as usize;
let slot_index = self.buckets[bucket_index]
.iter()
.position(|(k, _)| k == &key);
if let Some(slot_index) = slot_index {
self.buckets[bucket_index][slot_index].1 = value.clone();
} else {
self.buckets[bucket_index].push((key, value.clone()));
self.length += 1;
}
Ok(value)
}
pub fn retain_invoke(
&mut self,
vm: &mut Machine,
env: &Rc<Environment>,
predicate: &AnyFunction,
) -> Result<(), MachineError> {
for bucket in self.buckets.iter_mut() {
let mut index = 0;
loop {
if index >= bucket.len() {
break;
}
let (key, value) = &bucket[index];
let output = predicate.invoke(vm, env, &[key.clone(), value.clone()])?;
if !output.is_trueish() {
bucket.remove(index);
self.length -= 1;
} else {
index += 1;
}
}
}
Ok(())
}
pub fn get(&self, key: &Value) -> Option<&Value> {
let hash = key.hash()?;
let bucket_index = (hash % self.buckets.len() as u64) as usize;
self.buckets[bucket_index]
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v)
}
pub fn remove(&mut self, key: &Value) -> Option<Value> {
let hash = key.hash()?;
let bucket_index = (hash % self.buckets.len() as u64) as usize;
let slot_index = self.buckets[bucket_index]
.iter()
.position(|(k, _)| k == key)?;
let value = self.buckets[bucket_index].remove(slot_index);
self.length -= 1;
Some(value.1)
}
pub fn iter(&self) -> HashTableIter<'_> {
HashTableIter {
buckets: self.buckets.iter(),
inner: None,
}
}
pub fn iter_mut(&mut self) -> HashTableIterMut<'_> {
HashTableIterMut {
buckets: self.buckets.iter_mut(),
inner: None,
}
}
}
impl PartialEq for HashTableData {
fn eq(&self, other: &Self) -> bool {
if self.length != other.length {
return false;
}
for (key, value1) in self.iter() {
let Some(value2) = other.get(key) else {
return false;
};
if value1 != value2 {
return false;
}
}
true
}
}
impl fmt::Debug for HashTableData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
for bucket in self.buckets.iter() {
for cell in bucket.iter() {
list.entry(cell);
}
}
list.finish()
}
}
impl fmt::Display for HashTableData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut index = 0;
for bucket in self.buckets.iter() {
for (key, value) in bucket.iter() {
if index != 0 {
write!(f, " ")?;
}
write!(f, "({key} . {value})")?;
index += 1;
}
}
Ok(())
}
}
impl From<HashTableData> for HashTable {
fn from(value: HashTableData) -> Self {
Self(RefCell::new(value))
}
}
impl<'a> Iterator for HashTableIter<'a> {
type Item = (&'a Value, &'a Value);
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(inner) = &mut self.inner
&& let Some((key, value)) = inner.next()
{
return Some((key, value));
}
let next_bucket = self.buckets.next()?;
self.inner = Some(next_bucket.iter());
}
}
}
impl<'a> Iterator for HashTableIterMut<'a> {
type Item = (&'a Value, &'a mut Value);
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(inner) = &mut self.inner
&& let Some((key, value)) = inner.next()
{
return Some((key, value));
}
let next_bucket = self.buckets.next()?;
self.inner = Some(next_bucket.iter_mut());
}
}
}
+85 -6
View File
@@ -1,5 +1,6 @@
use std::{
fmt::{self, Write},
hash::{DefaultHasher, Hash, Hasher},
rc::Rc,
};
@@ -7,6 +8,7 @@ mod boolean;
mod closure;
mod cons;
mod function;
mod hashtable;
mod identifier;
mod iter;
mod keyword;
@@ -21,6 +23,7 @@ pub use boolean::BooleanValue;
pub use closure::{ClosureValue, UpvalueValue};
pub use cons::ConsCell;
pub use function::BytecodeFunction;
pub use hashtable::{HashTable, HashTableData};
pub use identifier::IdentifierValue;
pub use keyword::Keyword;
pub use native::{NativeFunction, NativeObject, NativeValue};
@@ -40,7 +43,6 @@ pub enum Value {
// Syntactic
Nil,
Cons(Rc<ConsCell>),
Vector(Rc<Vector>),
Number(NumberValue),
Boolean(BooleanValue),
Keyword(Keyword),
@@ -50,6 +52,9 @@ pub enum Value {
Quote(Rc<Value>),
Unquote(Rc<Value>),
UnquoteSplice(Rc<Value>),
// Collections
Vector(Rc<Vector>),
HashTable(Rc<HashTable>),
// Semantic
Closure(ClosureValue),
Function(Rc<BytecodeFunction>),
@@ -59,6 +64,73 @@ pub enum Value {
}
impl Value {
fn hash_inner<H: Hasher>(&self, state: &mut H) -> bool {
match self {
Self::Nil => state.write_u8(0),
Self::Cons(cons) => {
let ConsCell(car, cdr) = cons.as_ref();
state.write_u8(1);
if !car.hash_inner(state) {
return false;
}
if !cdr.hash_inner(state) {
return false;
}
}
Self::Number(value) => {
state.write_u8(2);
return value.hash_inner(state);
}
Self::Boolean(value) => {
state.write_u8(3);
value.hash(state);
}
Self::Keyword(value) => {
state.write_u8(4);
value.hash(state);
}
Self::Identifier(value) => {
state.write_u8(5);
value.hash(state);
}
Self::String(value) => {
state.write_u8(6);
value.hash(state);
}
Self::Quasi(value) => {
state.write_u8(7);
return value.hash_inner(state);
}
Self::Quote(value) => {
state.write_u8(8);
return value.hash_inner(state);
}
Self::Unquote(value) => {
state.write_u8(9);
return value.hash_inner(state);
}
Self::UnquoteSplice(value) => {
state.write_u8(10);
return value.hash_inner(state);
}
Self::Vector(_)
| Self::HashTable(_)
| Self::NativeFunction(_)
| Self::Function(_)
| Self::NativeValue(_)
| Self::Closure(_) => return false,
}
true
}
pub fn hash(&self) -> Option<u64> {
let mut hasher = DefaultHasher::new();
if !self.hash_inner(&mut hasher) {
return None;
}
Some(hasher.finish())
}
pub fn is_nil(&self) -> bool {
matches!(self, Self::Nil)
}
@@ -103,6 +175,7 @@ impl Value {
Value::list_or_nil([Self::Identifier("unquote-splice".into()), value.type_id()])
}
Self::Vector(_) => Self::Identifier("vector".into()),
Self::HashTable(_) => Self::Identifier("hash".into()),
Self::Keyword(_) => Self::Identifier("keyword".into()),
Self::String(_) => Self::Identifier("string".into()),
Self::NativeValue(_) => Self::Identifier("native".into()),
@@ -117,6 +190,7 @@ impl Value {
Self::Nil => false,
Self::Cons(_) => true,
Self::Vector(vector) => !vector.is_empty(),
Self::HashTable(table) => !table.borrow().is_empty(),
Self::Number(value) => value.is_trueish(),
Self::String(value) => !value.is_empty(),
Self::Boolean(BooleanValue(value)) => *value,
@@ -212,11 +286,6 @@ impl fmt::Display for Value {
fmt::Display::fmt(cons, f)?;
f.write_char(')')
}
Self::Vector(vector) => {
f.write_str("#[")?;
fmt::Display::fmt(vector, f)?;
f.write_char(']')
}
Self::Keyword(keyword) => write!(f, "{keyword}"),
Self::Identifier(identifier) => write!(f, "{identifier}"),
Self::String(value) => fmt::Display::fmt(value, f),
@@ -242,6 +311,16 @@ impl fmt::Display for Value {
Self::Function(value) => fmt::Display::fmt(value, f),
Self::NativeValue(value) => fmt::Display::fmt(value, f),
Self::NativeFunction(value) => fmt::Display::fmt(value, f),
Self::Vector(vector) => {
f.write_str("#[")?;
fmt::Display::fmt(vector, f)?;
f.write_char(']')
}
Self::HashTable(table) => {
f.write_str("#hash{")?;
fmt::Display::fmt(table, f)?;
f.write_char('}')
}
}
}
}
+16
View File
@@ -1,6 +1,7 @@
use std::{
cmp::Ordering,
fmt,
hash::{Hash, Hasher},
num::TryFromIntError,
ops::{Add, Div, DivAssign, Mul, Neg, Rem, RemAssign, Sub, SubAssign},
};
@@ -20,6 +21,21 @@ pub enum NumberValue {
}
impl NumberValue {
pub(super) fn hash_inner<H: Hasher>(&self, state: &mut H) -> bool {
match self {
Self::Int(value) => {
state.write_u8(1);
value.hash(state)
}
Self::Float(value) if !value.is_nan() => {
state.write_u8(2);
state.write(&value.to_le_bytes());
}
_ => return false,
}
true
}
pub const fn nan() -> Self {
Self::Float(f64::NAN)
}
+1 -1
View File
@@ -31,6 +31,6 @@ impl Deref for StringValue {
impl fmt::Display for StringValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.0.as_ref(), f)
fmt::Debug::fmt(self.0.as_ref(), f)
}
}