HyphaeVM Garbage Collection
All checks were successful
per-push tests / build (push) Successful in 36s
per-push tests / test-utility (push) Successful in 36s
per-push tests / test-frontend (push) Successful in 37s
per-push tests / test-backend (push) Successful in 31s
per-push tests / timed-decomposer-parse (push) Successful in 34s

This commit removes the dependency upon Mycelium::sexpr::Datum from Hyphae
and instead adds a heap.rs module including three types:

- Gc: This is essentially a Box<Rc<T>> but passed around as a *const Rc<T>.
    not only does this allow the wrapped T to be passed around in a format
    that fits completely within a single physical register, this also
    applies a greedy reference counting garbage collection to each and
    every object allocated in the VM.

- Datum: This is a simplified enum for type polymorphism. Similar to the
    original Mycelium::sexpr::Datum.

- Cons: This is a very standard Cons cell type.

Additionally, two new instructions are added:
- DUPL: a deep copy instruction
- CONST: an instruction to create number datum from embedded constants
- NTOI: casts a number to its inexact form
- NTOE: casts a number to its exact form

Fixes: #35 and #36

Signed-off-by: Ava Affine <ava@sunnypup.io>
This commit is contained in:
Ava Apples Affine 2025-07-26 01:13:13 +00:00
parent 8d2d0ebf0c
commit cf626b2bfc
7 changed files with 324 additions and 295 deletions

View file

@ -22,12 +22,11 @@ use crate::hmap::QuickMap;
use crate::stackstack::StackStack;
use crate::instr as i;
use crate::util::{Operand, Program, Address};
use crate::heap::Datum;
use crate::heap::{Gc, Datum};
use core::cell::RefCell;
use alloc::vec;
use alloc::rc::Rc;
use alloc::vec::Vec;
use alloc::sync::Arc;
use alloc::borrow::ToOwned;
@ -39,20 +38,20 @@ const NUM_OPERAND_REGISTERS: usize = 4;
pub struct VM {
// execution environment
pub stack: StackStack<Datum>,
pub stack: StackStack<Gc<Datum>>,
pub symtab: QuickMap<Operand>,
pub prog: Program,
pub fds: Vec<u64>,
pub traps: Vec<Arc<dyn Fn(&mut VM)>>,
// data registers
pub expr: Datum,
pub oper: [Datum; NUM_OPERAND_REGISTERS],
pub expr: Gc<Datum>,
pub oper: [Gc<Datum>; NUM_OPERAND_REGISTERS],
// control flow registers
pub retn: usize,
pub ictr: usize,
pub errr: Datum,
pub errr: Gc<Datum>,
// state
pub running: bool,
@ -86,13 +85,14 @@ impl VM {
{
self.running = false;
self.err_state = true;
self.errr = Datum::String($err.as_bytes().to_vec());
self.errr = Datum::String($err.as_bytes().to_vec()).into();
return;
}
}
}
macro_rules! deref {
// get or set according to addressing mode
macro_rules! access {
( $oper:expr ) => {
match $oper.0 {
Address::Expr => &self.expr,
@ -101,23 +101,19 @@ impl VM {
Address::Oper3 => &self.oper[2],
Address::Oper4 => &self.oper[3],
Address::Stack => &self.stack[$oper.1],
Address::Numer => e!("attempt to dereference constant numeric data"),
Address::Numer => e!("cannot access constant numeric"),
Address::Instr => e!("bad access to instruction data"),
}
}
}
};
macro_rules! deref_mut {
( $oper:expr ) => {
match $oper.0 {
Address::Expr => &mut self.expr,
Address::Oper1 => &mut self.oper[0],
Address::Oper2 => &mut self.oper[1],
Address::Oper3 => &mut self.oper[2],
Address::Oper4 => &mut self.oper[3],
Address::Instr => e!("bad mutable access to instruction data"),
// Stack, Numer
_ => e!("mutable access to immutable data"),
( $data:expr, $target:expr ) => {
match $data.0 {
Address::Expr => self.expr = $target,
Address::Oper1 => self.oper[0] = $target,
Address::Oper2 => self.oper[1] = $target,
Address::Oper3 => self.oper[2] = $target,
Address::Oper4 => self.oper[3] = $target,
_ => e!("attempted mutation of immutable address"),
}
}
}
@ -138,13 +134,13 @@ impl VM {
macro_rules! lr_oper {
( $in_type:ident, $oper:tt, $out_type:ident ) => {
self.expr = Datum::$out_type(*match deref!(&instr.1[0]){
self.expr = Datum::$out_type(match **access!(&instr.1[0]){
Datum::$in_type(l) => l,
_ => e!("illegal argument to instruction"),
} $oper *match deref!(&instr.1[1]){
} $oper match **access!(&instr.1[1]){
Datum::$in_type(l) => l,
_ => e!("illegal argument to instruction"),
})
}).into()
}
}
@ -163,38 +159,40 @@ impl VM {
// symtable ops
i::BIND => {
let Datum::String(tag) = deref!(&instr.1[0]) else {
let Datum::String(ref tag) = **access!(&instr.1[0]) else {
e!("illegal argument to BIND instruction");
};
let tag = unsafe { str::from_utf8_unchecked(&tag).to_owned() };
let tag = unsafe { str::from_utf8_unchecked(tag).to_owned() };
self.symtab.insert(tag, instr.1[1].clone());
},
i::UNBIND => {
let Datum::String(tag) = deref!(&instr.1[0]) else {
let Datum::String(ref tag) = **access!(&instr.1[0]) else {
e!("illegal argument to UNBIND instruction");
};
let tag = unsafe { str::from_utf8_unchecked(&tag) };
self.symtab.remove(&tag);
let tag = unsafe { str::from_utf8_unchecked(tag) };
self.symtab.remove(tag);
},
i::BOUND => {
let Datum::String(tag) = deref!(&instr.1[0]) else {
let Datum::String(ref tag) = **access!(&instr.1[0]) else {
e!("illegal argument to BOUND instruction");
};
let tag = unsafe { str::from_utf8_unchecked(&tag) };
self.symtab.contains_key(&tag);
let tag = unsafe { str::from_utf8_unchecked(tag) };
self.symtab.contains_key(tag);
},
// stack ops
i::PUSH => self.stack.push_current_stack(deref!(&instr.1[0]).clone()),
i::PUSH => self.stack.push_current_stack(
access!(&instr.1[0]).clone()),
i::POP => _ = self.stack.pop_current_stack(),
i::ENTER => self.stack.add_stack(),
i::EXIT => self.stack.destroy_top_stack(),
// movement ops
i::LOAD => *deref_mut!(&instr.1[1]) = deref!(&instr.1[0]).clone(),
i::CLEAR => *deref_mut!(&instr.1[0]) = Datum::None,
i::LOAD => access!(&instr.1[1], access!(&instr.1[0]).clone()),
i::DUPL => access!(&instr.1[1], access!(&instr.1[0]).deep_copy()),
i::CLEAR => access!(&instr.1[0], Datum::None.into()),
// control flow ops
i::NOP => (),
@ -202,7 +200,7 @@ impl VM {
i::PANIC => {
self.running = false;
self.err_state = false;
self.errr = deref!(&instr.1[0]).clone()
self.errr = access!(&instr.1[0]).clone();
},
i::JMP => {
@ -210,24 +208,25 @@ impl VM {
},
i::JMPIF => {
if let Datum::Bool(true) = self.expr {
if let Datum::Bool(true) = *self.expr {
do_jmp!(0);
}
},
// boolean ops
i::EQ => self.expr = Datum::Bool(*deref!(&instr.1[0]) == *deref!(&instr.1[1])),
i::EQ => self.expr =
Datum::Bool(*access!(&instr.1[0]) == *access!(&instr.1[1])).into(),
i::LT => lr_oper!(Number, <, Bool),
i::GT => lr_oper!(Number, >, Bool),
i::LTE => lr_oper!(Number, <=, Bool),
i::GTE => lr_oper!(Number, >=, Bool),
i::BOOL_NOT => {
self.expr = Datum::Bool(!{
let Datum::Bool(a) = self.expr else {
let Datum::Bool(a) = *self.expr else {
e!("illegal argument to BOOL_NOT instruction");
};
a
});
}).into();
},
i::BOOL_AND => lr_oper!(Bool, &&, Bool),
@ -239,11 +238,11 @@ impl VM {
i::XOR => lr_oper!(Char, ^, Char),
i::BYTE_NOT => {
self.expr = Datum::Char(!{
let Datum::Char(a) = self.expr else {
let Datum::Char(a) = *self.expr else {
e!("illegal argument to BYTE_NOT instruction");
};
a
});
}).into();
},
// numeric ops
@ -252,11 +251,11 @@ impl VM {
i::MUL => lr_oper!(Number, *, Number),
i::FDIV => lr_oper!(Number, /, Number),
i::IDIV => {
let Datum::Number(l) = deref!(&instr.1[0]) else {
let Datum::Number(ref l) = **access!(&instr.1[0]) else {
e!("illegal argument to IDIV instruction");
};
let Datum::Number(r) = deref!(&instr.1[1]) else {
let Datum::Number(ref r) = **access!(&instr.1[1]) else {
e!("illgal argument to IDIV instruction");
};
@ -268,61 +267,87 @@ impl VM {
e!("integer division on non integer value");
};
self.expr = Datum::Number(Number::Fra(Fraction(l / r, 1)));
self.expr = Datum::Number(Number::Fra(Fraction(l / r, 1))).into();
},
i::POW => {
let Datum::Number(l) = deref!(&instr.1[0]) else {
let Datum::Number(ref l) = **access!(&instr.1[0]) else {
e!("illegal argument to POW instruction");
};
let Datum::Number(r) = deref!(&instr.1[1]) else {
let Datum::Number(ref r) = **access!(&instr.1[1]) else {
e!("illgal argument to POW instruction");
};
self.expr = Datum::Number((*l).pow(*r));
self.expr = Datum::Number(l.clone().pow(r.clone())).into();
},
i::INC => if let Datum::Number(src) = deref_mut!(&instr.1[0]) {
*src = *src + Number::Fra(Fraction(1, 1));
} else {
i::INC => access!(&instr.1[0], {
if let Datum::Number(src) = **access!(&instr.1[0]) {
Datum::Number(src + Number::Fra(Fraction(1, 1))).into()
} else {
e!("illegal argument to INC instruction");
},
}
}),
i::DEC => if let Datum::Number(src) = deref_mut!(&instr.1[0]) {
*src = *src - Number::Fra(Fraction(1, 1));
} else {
i::DEC => access!(&instr.1[0], {
if let Datum::Number(src) = **access!(&instr.1[0]) {
Datum::Number(src - Number::Fra(Fraction(1, 1))).into()
} else {
e!("illegal argument to INC instruction");
},
}
}),
// byte/char to and from number conversions
i::CTON => {
let src = deref_mut!(&instr.1[0]);
if let Datum::Char(schr) = src {
*src = Datum::Number(Number::Fra(Fraction(*schr as isize, 1)));
i::CTON => access!(&instr.1[0], {
if let Datum::Char(schr) = **access!(&instr.1[0]) {
Datum::Number(Number::Fra(Fraction(schr as isize, 1))).into()
} else {
e!("illegal argument to CTON instruction");
e!("illegal argument to INC instruction");
}
},
}),
i::NTOC => {
let src = deref_mut!(&instr.1[0]);
if let Datum::Number(snum) = src {
i::NTOC => access!(&instr.1[0], {
if let Datum::Number(snum) = **access!(&instr.1[0]) {
let n = snum.make_inexact();
if !snum.is_exact() || n.0.fract() != 0.0 || n.0 > u8::MAX.into() || n.0 < 0.0 {
e!("input to NTOC cannot cleanly convert");
}
*src = Datum::Char(n.0.trunc() as u64 as u8);
if !snum.is_exact() || n.0.fract() != 0.0 ||
n.0 > u8::MAX.into() || n.0 < 0.0 {
e!("input to NTOC cannot cleanly convert");
}
Datum::Char(n.0.trunc() as u64 as u8).into()
} else {
e!("illegal argument to NTOC instruction");
e!("illegal argument to INC instruction");
}
}),
i::NTOI => {
let src = access!(&instr.1[0]);
if let Datum::Number(snum) = **src {
access!(&instr.1[0],
Datum::Number(snum.make_inexact().into()).into())
}
},
i::MKVEC => self.expr = Datum::Vector(RefCell::from(vec![])),
i::MKBVEC => self.expr = Datum::ByteVector(RefCell::from(vec![])),
i::NTOE => {
let src = access!(&instr.1[0]);
if let Datum::Number(snum) = **src {
access!(&instr.1[0], Datum::Number(snum.make_inexact().into())
.into())
}
},
i::CONST => access!(&instr.1[0], {
let Operand(Address::Numer, num) = instr.1[0] else {
e!("illegal argument to CONST instruction");
};
Datum::Number(Number::Fra(Fraction(num as isize, 1))).into()
}),
i::MKVEC => self.expr = Datum::Vector(RefCell::from(vec![])).into(),
i::MKBVEC => self.expr = Datum::ByteVector(RefCell::from(vec![])).into(),
i::INDEX => {
let Datum::Number(idx) = deref!(&instr.1[1]) else {
let Datum::Number(ref idx) = **access!(&instr.1[1]) else {
e!("illegal argument to INDEX instruction");
};
let idx = idx.make_inexact();
@ -331,40 +356,43 @@ impl VM {
}
let idx = idx.0.trunc() as usize;
match deref!(&instr.1[0]) {
Datum::Vector(v) => {
match **access!(&instr.1[0]) {
Datum::Vector(ref v) => {
let a = (*v.borrow()[idx].clone()).clone();
self.expr = a;
self.expr = a.into();
},
Datum::ByteVector(bv) => {
Datum::ByteVector(ref bv) => {
let a = Datum::Char(bv.borrow()[idx]);
self.expr = a;
self.expr = a.into();
},
Datum::List(l) => self.expr = l[idx].clone(),
Datum::Cons(ref l) => self.expr = l[idx].clone(),
_ => e!("illegal argument to INDEX instruction")
};
},
i::LENGTH => match deref!(&instr.1[0]) {
Datum::Vector(v) => {
let a = Datum::Number(Number::Fra(Fraction(v.borrow().len() as isize, 1)));
self.expr = a;
i::LENGTH => match **access!(&instr.1[0]) {
Datum::Vector(ref v) => {
let a = Datum::Number(Number::Fra(Fraction(
v.borrow().len() as isize, 1)));
self.expr = a.into();
},
Datum::ByteVector(bv) => {
let a = Datum::Number(Number::Fra(Fraction(bv.borrow().len() as isize, 1)));
self.expr = a;
Datum::ByteVector(ref bv) => {
let a = Datum::Number(Number::Fra(Fraction(
bv.borrow().len() as isize, 1)));
self.expr = a.into();
},
Datum::List(l) =>
self.expr = Datum::Number(Number::Fra(Fraction(l.len() as isize, 1))),
Datum::Cons(ref l) => self.expr =
Datum::Number(Number::Fra(Fraction(l.len() as isize, 1)))
.into(),
_ => e!("illegal argument to LENGTH instruction"),
},
i::SUBSL => {
let Datum::Number(st) = deref!(&instr.1[1]) else {
let Datum::Number(ref st) = **access!(&instr.1[1]) else {
e!("illegal argument to SUBSL instruction");
};
let Datum::Number(ed) = deref!(&instr.1[2]) else {
let Datum::Number(ref ed) = **access!(&instr.1[2]) else {
e!("illegal argument to SUBSL instruction");
};
@ -382,24 +410,24 @@ impl VM {
let st = st.0.trunc() as usize;
let ed = ed.0.trunc() as usize;
match deref!(&instr.1[0]) {
Datum::Vector(v) => {
match **access!(&instr.1[0]) {
Datum::Vector(ref v) => {
let a = Datum::Vector(RefCell::from(v.borrow()[st..ed].to_vec()));
self.expr = a;
self.expr = a.into();
},
Datum::ByteVector(bv) => {
Datum::ByteVector(ref bv) => {
let a = Datum::ByteVector(RefCell::from(bv.borrow()[st..ed].to_vec()));
self.expr = a;
self.expr = a.into();
},
Datum::List(a) =>
self.expr = Datum::List(Rc::new(
(**a).subsl(st as isize, ed as isize))),
Datum::Cons(ref a) => self.expr =
Datum::Cons(a.subsl(st as isize, ed as isize)).into(),
_ => e!("illegal argument to SUBSL instruction")
};
}
i::INSER => {
let Datum::Number(idx) = deref!(&instr.1[2]) else {
let Datum::Number(ref idx) = **access!(&instr.1[2]) else {
e!("illegal argument to INSER instruction");
};
@ -410,34 +438,40 @@ impl VM {
let idx = idx.0.trunc() as usize;
match deref!(&instr.1[0]) {
Datum::Vector(v) => {
v.borrow_mut().insert(idx, deref!(&instr.1[1]).clone().into());
match **access!(&instr.1[0]) {
Datum::Vector(ref v) => {
v.borrow_mut()
.insert(idx, access!(&instr.1[1])
.deep_copy());
},
Datum::ByteVector(bv) => {
let Datum::Char(b) = deref!(&instr.1[1]) else {
Datum::ByteVector(ref bv) => {
let Datum::Char(b) = **access!(&instr.1[1]) else {
e!("INSER instruction can only insert a byte into a bytevector");
};
bv.borrow_mut().insert(idx, *b);
bv.borrow_mut().insert(idx, b);
},
_ => e!("illegal argument to INSER instruction")
}
},
i::CAR => {
let Datum::List(arg) = deref!(&instr.1[0]) else {
let Datum::Cons(ref arg) = **access!(&instr.1[0]) else {
e!("illegal argument to CAR instruction");
};
self.expr = (*arg.0).clone();
self.expr = arg.clone().0
.or(Some(Datum::None.into()))
.expect("CAR instruction option consistency");
},
i::CDR => {
let Datum::List(arg) = deref!(&instr.1[0]) else {
let Datum::Cons(ref arg) = **access!(&instr.1[0]) else {
e!("illegal argument to CAR instruction");
};
self.expr = (*arg.1).clone();
self.expr = arg.clone().1
.or(Some(Datum::None.into()))
.expect("CDR instruction option consistency");
},
i::CONS => {
@ -447,10 +481,6 @@ impl VM {
*/
},
// in order to maintain a language agnostic VM these must be traps
//i::PARSE => todo!("implement AST API"),
//i::EVAL => todo!("implement AST API"),
_ => {
e!("illegal instruction");
},