Redesign the engine (#56)
This commit is contained in:
parent
ddb957b16a
commit
d335667d41
|
@ -8,6 +8,7 @@ logos = "0.12.0"
|
|||
ariadne = "0.1.5"
|
||||
hashbrown = { version = "0.12.0", default-features = false, features = ["nightly"] }
|
||||
cstree = "0.11.0"
|
||||
rand = "0.8.5"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.12.0"
|
||||
|
@ -20,7 +21,3 @@ debug = true
|
|||
[[bench]]
|
||||
name = "parse"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "gc"
|
||||
harness = false
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
#![feature(int_log)]
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Criterion, Throughput};
|
||||
use zaia::engine::{
|
||||
gc::{Heap, Trace},
|
||||
value::{Table, Value},
|
||||
};
|
||||
|
||||
const OBJECTS_COUNT: u64 = 100000;
|
||||
const GRAPH_BREADTH: u64 = 3;
|
||||
|
||||
fn mark(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("mark");
|
||||
let depth = OBJECTS_COUNT.log(GRAPH_BREADTH) as u64;
|
||||
let actual_count = GRAPH_BREADTH.pow(depth as u32);
|
||||
group.throughput(Throughput::Elements(actual_count));
|
||||
group.bench_function("mark", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
fn widen(heap: &Heap, node: &mut Table, depth: u64) {
|
||||
if depth == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
for i in 0..GRAPH_BREADTH {
|
||||
let mut table = Table::new(heap.clone());
|
||||
widen(heap, &mut table, depth - 1);
|
||||
let value = Value::from_table(heap.insert(table));
|
||||
node.insert(Value::from_int(i as i32), value);
|
||||
}
|
||||
}
|
||||
|
||||
let heap = Heap::new();
|
||||
let mut root = Table::new(heap.clone());
|
||||
widen(&heap, &mut root, depth);
|
||||
let handle = heap.insert(root);
|
||||
(heap, handle)
|
||||
},
|
||||
|(heap, handle)| {
|
||||
heap.collect(
|
||||
|visitor| {
|
||||
unsafe {
|
||||
handle.get_unchecked_mut().visit(visitor);
|
||||
}
|
||||
|
||||
visitor.mark(handle.tagged());
|
||||
},
|
||||
|_| unreachable!(),
|
||||
);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn sweep(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sweep");
|
||||
group.throughput(Throughput::Elements(OBJECTS_COUNT));
|
||||
group.bench_function("sweep", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let heap = Heap::new();
|
||||
|
||||
for i in 0..OBJECTS_COUNT {
|
||||
match i % 3 {
|
||||
0 | 1 => {
|
||||
heap.insert_string(b"Foo Fighters!");
|
||||
},
|
||||
2 => {
|
||||
let mut table = Table::new(heap.clone());
|
||||
let i = i as i32;
|
||||
table.insert(Value::from_int(i), Value::from_int(i));
|
||||
table.insert(Value::from_int(i + 1), Value::from_int(i + 1));
|
||||
heap.insert(table);
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
heap
|
||||
},
|
||||
|heap| {
|
||||
heap.collect(|_| (), |_| ());
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, mark, sweep);
|
||||
criterion_main!(benches);
|
|
@ -8,7 +8,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
|||
let source = fs::read_to_string("test-files/mixed.lua").unwrap();
|
||||
let mut group = c.benchmark_group("parse");
|
||||
let mut deferred = Vec::new();
|
||||
group.throughput(Throughput::Elements(source.lines().count() as u64));
|
||||
group.throughput(Throughput::Bytes(source.len() as u64));
|
||||
group.bench_function("mixed.lua", |b| {
|
||||
b.iter(|| parse_mixed(black_box(&source), &mut deferred));
|
||||
});
|
||||
|
|
|
@ -10,4 +10,3 @@ normalize_doc_attributes = true
|
|||
reorder_impl_items = true
|
||||
group_imports = "StdExternalCrate"
|
||||
use_field_init_shorthand = true
|
||||
wrap_comments = true
|
||||
|
|
69
src/engine/bytecode.rs
Normal file
69
src/engine/bytecode.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
#[derive(Clone, Copy)]
|
||||
pub struct Register(pub u8);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Upvalue(pub u8);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Chunk(pub u16);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Constant(pub u16);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ArgumentCount(pub u8);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct InstructionIndex(pub u32);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Capture {
|
||||
pub is_upvalue: bool,
|
||||
pub slot: Register,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum Instruction {
|
||||
ISLT(Register, Register),
|
||||
ISLE(Register, Register),
|
||||
ISEQ(Register, Register),
|
||||
ISNEQ(Register, Register),
|
||||
IST(Register),
|
||||
ISF(Register),
|
||||
MOV(Register, Register),
|
||||
NOT(Register, Register),
|
||||
NEG(Register, Register),
|
||||
LEN(Register, Register),
|
||||
BITNOT(Register, Register),
|
||||
ADD(Register, Register, Register),
|
||||
SUB(Register, Register, Register),
|
||||
MUL(Register, Register, Register),
|
||||
DIV(Register, Register, Register),
|
||||
INTDIV(Register, Register, Register),
|
||||
EXP(Register, Register, Register),
|
||||
MOD(Register, Register, Register),
|
||||
BITAND(Register, Register, Register),
|
||||
BITOR(Register, Register, Register),
|
||||
BITXOR(Register, Register, Register),
|
||||
LEFTSHIFT(Register, Register, Register),
|
||||
RIGHTSHIFT(Register, Register, Register),
|
||||
AND(Register, Register, Register),
|
||||
OR(Register, Register, Register),
|
||||
CONCAT(Register, Register, Register),
|
||||
CLOAD(Register, Constant),
|
||||
UGET(Register, Upvalue),
|
||||
USET(Register, Upvalue),
|
||||
FNEW(Register, Chunk, Vec<Capture>),
|
||||
TNEW(Register),
|
||||
TGET(Register, Register, Register),
|
||||
TSET(Register, Register, Register),
|
||||
GGET(Register, Register),
|
||||
GSET(Register, Register),
|
||||
JMP(InstructionIndex),
|
||||
APUSH(Register),
|
||||
APOP(Register),
|
||||
ACLEAR,
|
||||
CALL(Register),
|
||||
RET,
|
||||
}
|
|
@ -1,4 +1,18 @@
|
|||
pub enum Error {
|
||||
UncaughtBreak,
|
||||
UncaughtReturn,
|
||||
pub struct Error {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new<S>(message: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
Self {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
use std::{cmp, fmt, hash};
|
||||
|
||||
use super::super::util;
|
||||
|
||||
/// # Safety
|
||||
/// `PtrTag` must be implemented directly using the functions
|
||||
/// found in the `encoding` submodule.
|
||||
pub unsafe trait PtrTag {
|
||||
fn is(x: u64) -> bool;
|
||||
fn tag(x: usize) -> u64;
|
||||
}
|
||||
|
||||
pub struct Handle<T>
|
||||
where
|
||||
T: ?Sized + PtrTag,
|
||||
{
|
||||
ptr: *mut T,
|
||||
}
|
||||
|
||||
impl<T> Handle<T>
|
||||
where
|
||||
T: ?Sized + PtrTag,
|
||||
{
|
||||
pub fn new(ptr: *mut T) -> Self {
|
||||
Handle { ptr }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// - Handle must point to a living instance of `T`.
|
||||
pub unsafe fn get_unchecked<'a>(self) -> &'a T {
|
||||
&*self.ptr
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// - Handle must point to a living instance of `T`.
|
||||
pub unsafe fn get_unchecked_mut<'a>(self) -> &'a mut T {
|
||||
&mut *self.ptr
|
||||
}
|
||||
|
||||
pub fn as_ptr(self) -> *mut T {
|
||||
self.ptr
|
||||
}
|
||||
|
||||
pub fn tagged(self) -> TaggedHandle {
|
||||
let tagged = T::tag(self.ptr as *mut u8 as usize);
|
||||
TaggedHandle::new(tagged)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for Handle<T>
|
||||
where
|
||||
T: ?Sized + PtrTag,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Handle({:p})", self.ptr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for Handle<T>
|
||||
where
|
||||
T: ?Sized + PtrTag,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Handle { ptr: self.ptr }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for Handle<T> where T: ?Sized + PtrTag {}
|
||||
|
||||
impl<T> cmp::PartialEq for Handle<T>
|
||||
where
|
||||
T: ?Sized + PtrTag,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.ptr == other.ptr
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> cmp::Eq for Handle<T> where T: ?Sized + PtrTag {}
|
||||
|
||||
impl<T> hash::Hash for Handle<T>
|
||||
where
|
||||
T: ?Sized + PtrTag,
|
||||
{
|
||||
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||
self.ptr.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TaggedHandle {
|
||||
tagged: u64,
|
||||
}
|
||||
|
||||
impl TaggedHandle {
|
||||
pub fn new(tagged: u64) -> Self {
|
||||
Self { tagged }
|
||||
}
|
||||
|
||||
pub fn value(self) -> u64 {
|
||||
self.tagged
|
||||
}
|
||||
|
||||
pub fn hash(self) -> u64 {
|
||||
util::mix_u64(self.tagged as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TaggedHandle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "TaggedHandle({:x})", self.tagged)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for TaggedHandle {
|
||||
fn clone(&self) -> Self {
|
||||
TaggedHandle {
|
||||
tagged: self.tagged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Copy for TaggedHandle {}
|
||||
|
||||
impl cmp::PartialEq for TaggedHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.tagged == other.tagged
|
||||
}
|
||||
}
|
||||
|
||||
impl cmp::Eq for TaggedHandle {}
|
|
@ -1,50 +0,0 @@
|
|||
use std::cell::Cell;
|
||||
|
||||
const INITIAL_THRESHOLD: usize = 128 * 1024;
|
||||
const THRESHOLD_FACTOR: f32 = 1.75;
|
||||
|
||||
pub struct Heuristics {
|
||||
allocated: Cell<usize>,
|
||||
threshold: Cell<usize>,
|
||||
should_collect: Cell<bool>,
|
||||
}
|
||||
|
||||
impl Heuristics {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
allocated: Cell::new(0),
|
||||
threshold: Cell::new(INITIAL_THRESHOLD),
|
||||
should_collect: Cell::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn threshold(&self) -> usize {
|
||||
(self.threshold.get() as f32 * THRESHOLD_FACTOR) as usize
|
||||
}
|
||||
|
||||
pub fn adjust(&self) {
|
||||
let new_threshold = self.threshold();
|
||||
self.threshold.set(new_threshold);
|
||||
self.should_collect.set(false);
|
||||
}
|
||||
|
||||
fn check_collect(&self) {
|
||||
if self.allocated >= self.threshold {
|
||||
self.should_collect.set(true);
|
||||
let new_threshold = self.threshold();
|
||||
self.threshold.set(new_threshold);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_allocated<F>(&self, f: F)
|
||||
where
|
||||
F: FnOnce(usize) -> usize,
|
||||
{
|
||||
self.allocated.update(f);
|
||||
self.check_collect();
|
||||
}
|
||||
|
||||
pub fn should_collect(&self) -> bool {
|
||||
self.should_collect.get()
|
||||
}
|
||||
}
|
|
@ -1,346 +0,0 @@
|
|||
mod handle;
|
||||
mod heuristics;
|
||||
mod set;
|
||||
mod trace;
|
||||
|
||||
use std::{alloc, cell::RefCell, ptr, rc::Rc};
|
||||
|
||||
pub use handle::{Handle, PtrTag, TaggedHandle};
|
||||
use heuristics::Heuristics;
|
||||
use set::ObjectSet;
|
||||
pub use trace::{Trace, Visitor};
|
||||
|
||||
use super::value::{encoding, ByteString, Function, Table, Userdata};
|
||||
|
||||
pub struct Heap {
|
||||
internal: Rc<HeapInternal>,
|
||||
}
|
||||
|
||||
impl Heap {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Heap {
|
||||
internal: Rc::new(HeapInternal::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert<T>(&self, value: T) -> Handle<T>
|
||||
where
|
||||
T: PtrTag,
|
||||
{
|
||||
self.internal.insert(value)
|
||||
}
|
||||
|
||||
pub fn insert_string(&self, bytes: &[u8]) -> Handle<ByteString> {
|
||||
self.internal.insert_string(bytes)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// - Handle must point to a living instance of `T`.
|
||||
pub unsafe fn destroy(&self, handle: TaggedHandle) {
|
||||
self.internal.destroy(handle);
|
||||
}
|
||||
|
||||
pub fn collect<F1, F2>(&self, trace: F1, finalize: F2)
|
||||
where
|
||||
F1: FnOnce(&mut Visitor),
|
||||
F2: FnMut(TaggedHandle),
|
||||
{
|
||||
self.internal.collect(trace, finalize);
|
||||
}
|
||||
|
||||
pub fn should_collect(&self) -> bool {
|
||||
self.internal.heuristics.should_collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Heap {
|
||||
fn clone(&self) -> Self {
|
||||
Heap {
|
||||
internal: self.internal.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Tree {
|
||||
objects: ObjectSet,
|
||||
visitor: Visitor,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
fn collect<F1, F2>(&mut self, heap: &HeapInternal, trace: F1, mut finalize: F2)
|
||||
where
|
||||
F1: FnOnce(&mut Visitor),
|
||||
F2: FnMut(TaggedHandle),
|
||||
{
|
||||
trace(&mut self.visitor);
|
||||
|
||||
for object in self.visitor.unmarked(&self.objects) {
|
||||
finalize(object);
|
||||
self.objects.remove(object);
|
||||
|
||||
unsafe {
|
||||
heap.destroy(object);
|
||||
}
|
||||
}
|
||||
|
||||
self.visitor.reset();
|
||||
}
|
||||
}
|
||||
|
||||
struct HeapInternal {
|
||||
heuristics: Heuristics,
|
||||
tree: RefCell<Tree>,
|
||||
}
|
||||
|
||||
impl HeapInternal {
|
||||
fn new() -> Self {
|
||||
let tree = RefCell::new(Tree {
|
||||
objects: ObjectSet::new(),
|
||||
visitor: Visitor::new(),
|
||||
});
|
||||
|
||||
Self {
|
||||
heuristics: Heuristics::new(),
|
||||
tree,
|
||||
}
|
||||
}
|
||||
|
||||
fn insert<T>(&self, value: T) -> Handle<T>
|
||||
where
|
||||
T: PtrTag,
|
||||
{
|
||||
let ptr = Box::into_raw(Box::new_in(value, self));
|
||||
let handle = Handle::new(ptr);
|
||||
self.tree.borrow_mut().objects.insert(handle.tagged());
|
||||
handle
|
||||
}
|
||||
|
||||
fn insert_string(&self, bytes: &[u8]) -> Handle<ByteString> {
|
||||
let len = bytes.len() as u32;
|
||||
let layout = ByteString::layout(len);
|
||||
|
||||
unsafe {
|
||||
let ptr = alloc::Allocator::allocate(self, layout).unwrap().as_ptr() as *mut ByteString;
|
||||
ByteString::initialize_into(ptr, len);
|
||||
ptr::copy_nonoverlapping(bytes.as_ptr(), (&mut *ptr).offset(0), len as usize);
|
||||
let handle = Handle::new(ptr);
|
||||
self.tree.borrow_mut().objects.insert(handle.tagged());
|
||||
handle
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn destroy(&self, handle: TaggedHandle) {
|
||||
let tagged = handle.value();
|
||||
|
||||
match tagged {
|
||||
_ if encoding::is_string(tagged) => {
|
||||
let ptr = encoding::get_string(tagged) as *mut ByteString;
|
||||
let len = (*ptr).len();
|
||||
let layout = ByteString::layout(len as u32);
|
||||
let ptr_nn = ptr::NonNull::new_unchecked(ptr as *mut u8);
|
||||
alloc::Allocator::deallocate(self, ptr_nn, layout);
|
||||
},
|
||||
_ if encoding::is_table(tagged) => {
|
||||
let ptr = encoding::get_table(tagged) as *mut Table;
|
||||
Box::from_raw_in(ptr, self);
|
||||
},
|
||||
_ if encoding::is_function(tagged) => {
|
||||
let ptr = encoding::get_function(tagged) as *mut Function;
|
||||
Box::from_raw_in(ptr, self);
|
||||
},
|
||||
_ if encoding::is_userdata(tagged) => {
|
||||
let ptr = encoding::get_userdata(tagged) as *mut Userdata;
|
||||
Box::from_raw_in(ptr, self);
|
||||
},
|
||||
_ => panic!("unknown pointer type {:b}", tagged),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect<F1, F2>(&self, trace: F1, finalize: F2)
|
||||
where
|
||||
F1: FnOnce(&mut Visitor),
|
||||
F2: FnMut(TaggedHandle),
|
||||
{
|
||||
self.tree.borrow_mut().collect(self, trace, finalize);
|
||||
self.heuristics.adjust();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl alloc::Allocator for Heap {
|
||||
fn allocate(&self, layout: alloc::Layout) -> Result<ptr::NonNull<[u8]>, alloc::AllocError> {
|
||||
self.internal.allocate(layout)
|
||||
}
|
||||
|
||||
unsafe fn deallocate(&self, ptr: ptr::NonNull<u8>, layout: alloc::Layout) {
|
||||
self.internal.deallocate(ptr, layout)
|
||||
}
|
||||
|
||||
unsafe fn grow(
|
||||
&self,
|
||||
ptr: ptr::NonNull<u8>,
|
||||
old_layout: alloc::Layout,
|
||||
new_layout: alloc::Layout,
|
||||
) -> Result<ptr::NonNull<[u8]>, alloc::AllocError> {
|
||||
self.internal.grow(ptr, old_layout, new_layout)
|
||||
}
|
||||
|
||||
unsafe fn grow_zeroed(
|
||||
&self,
|
||||
ptr: ptr::NonNull<u8>,
|
||||
old_layout: alloc::Layout,
|
||||
new_layout: alloc::Layout,
|
||||
) -> Result<ptr::NonNull<[u8]>, alloc::AllocError> {
|
||||
self.internal.grow_zeroed(ptr, old_layout, new_layout)
|
||||
}
|
||||
|
||||
unsafe fn shrink(
|
||||
&self,
|
||||
ptr: ptr::NonNull<u8>,
|
||||
old_layout: alloc::Layout,
|
||||
new_layout: alloc::Layout,
|
||||
) -> Result<ptr::NonNull<[u8]>, alloc::AllocError> {
|
||||
self.internal.shrink(ptr, old_layout, new_layout)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl alloc::Allocator for HeapInternal {
|
||||
fn allocate(&self, layout: alloc::Layout) -> Result<ptr::NonNull<[u8]>, alloc::AllocError> {
|
||||
self.heuristics.update_allocated(|x| x + layout.size());
|
||||
alloc::Global.allocate(layout)
|
||||
}
|
||||
|
||||
unsafe fn deallocate(&self, ptr: ptr::NonNull<u8>, layout: alloc::Layout) {
|
||||
self.heuristics.update_allocated(|x| x - layout.size());
|
||||
|
||||
alloc::Global.deallocate(ptr, layout)
|
||||
}
|
||||
|
||||
unsafe fn grow(
|
||||
&self,
|
||||
ptr: ptr::NonNull<u8>,
|
||||
old_layout: alloc::Layout,
|
||||
new_layout: alloc::Layout,
|
||||
) -> Result<ptr::NonNull<[u8]>, alloc::AllocError> {
|
||||
self.heuristics
|
||||
.update_allocated(|x| x + new_layout.size() - old_layout.size());
|
||||
|
||||
alloc::Global.grow(ptr, old_layout, new_layout)
|
||||
}
|
||||
|
||||
unsafe fn grow_zeroed(
|
||||
&self,
|
||||
ptr: ptr::NonNull<u8>,
|
||||
old_layout: alloc::Layout,
|
||||
new_layout: alloc::Layout,
|
||||
) -> Result<ptr::NonNull<[u8]>, alloc::AllocError> {
|
||||
self.heuristics
|
||||
.update_allocated(|x| x + new_layout.size() - old_layout.size());
|
||||
|
||||
alloc::Global.grow_zeroed(ptr, old_layout, new_layout)
|
||||
}
|
||||
|
||||
unsafe fn shrink(
|
||||
&self,
|
||||
ptr: ptr::NonNull<u8>,
|
||||
old_layout: alloc::Layout,
|
||||
new_layout: alloc::Layout,
|
||||
) -> Result<ptr::NonNull<[u8]>, alloc::AllocError> {
|
||||
self.heuristics
|
||||
.update_allocated(|x| x + new_layout.size() - old_layout.size());
|
||||
|
||||
alloc::Global.shrink(ptr, old_layout, new_layout)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HeapInternal {
|
||||
fn drop(&mut self) {
|
||||
let tree = self.tree.borrow();
|
||||
tree.objects.iter().for_each(|object| unsafe {
|
||||
self.destroy(object);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
super::value::{Table, Value},
|
||||
Heap,
|
||||
};
|
||||
use crate::engine::gc::Trace;
|
||||
|
||||
#[test]
|
||||
fn collect_no_trace() {
|
||||
let heap = Heap::new();
|
||||
let table1 = Table::new(heap.clone());
|
||||
let table2 = Table::new(heap.clone());
|
||||
|
||||
heap.insert(table1);
|
||||
heap.insert(table2);
|
||||
|
||||
let mut ctr = 0;
|
||||
heap.collect(|_| (), |_| ctr += 1);
|
||||
assert_eq!(ctr, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_mark_direct() {
|
||||
let heap = Heap::new();
|
||||
let table1 = Table::new(heap.clone());
|
||||
let table2 = Table::new(heap.clone());
|
||||
let table3 = Table::new(heap.clone());
|
||||
let table4 = Table::new(heap.clone());
|
||||
|
||||
let handle1 = heap.insert(table1).tagged();
|
||||
heap.insert(table2);
|
||||
heap.insert(table3);
|
||||
heap.insert(table4);
|
||||
|
||||
let mut ctr = 0;
|
||||
heap.collect(|visitor| visitor.mark(handle1), |_| ctr += 1);
|
||||
assert_eq!(ctr, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_mark_indirect() {
|
||||
let heap = Heap::new();
|
||||
let table1 = Table::new(heap.clone());
|
||||
let table2 = Table::new(heap.clone());
|
||||
let table3 = Table::new(heap.clone());
|
||||
|
||||
let handle1 = heap.insert(table1);
|
||||
let handle2 = heap.insert(table2);
|
||||
let handle3 = heap.insert(table3);
|
||||
|
||||
let k1 = Value::from_int(3);
|
||||
let k2 = Value::from_int(5);
|
||||
|
||||
let tab = unsafe { handle1.get_unchecked_mut() };
|
||||
tab.insert(k1, Value::from_table(handle2));
|
||||
tab.insert(k2, Value::from_table(handle3));
|
||||
|
||||
let mut ctr = 0;
|
||||
macro_rules! collect {
|
||||
() => {{
|
||||
heap.collect(
|
||||
|visitor| {
|
||||
let handle = handle1.tagged();
|
||||
visitor.mark(handle);
|
||||
tab.visit(visitor);
|
||||
},
|
||||
|_| ctr += 1,
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
collect!();
|
||||
assert_eq!(ctr, 0);
|
||||
|
||||
tab.remove(k1);
|
||||
tab.remove(k2);
|
||||
|
||||
collect!();
|
||||
assert_eq!(ctr, 2);
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
use hashbrown::{hash_map, HashMap};
|
||||
|
||||
use super::TaggedHandle;
|
||||
|
||||
pub struct ObjectSet {
|
||||
set: HashMap<TaggedHandle, (), ()>,
|
||||
}
|
||||
|
||||
impl ObjectSet {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
set: HashMap::with_hasher(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_mut(
|
||||
&mut self,
|
||||
handle: TaggedHandle,
|
||||
) -> hash_map::RawEntryMut<'_, TaggedHandle, (), ()> {
|
||||
let hash = handle.hash();
|
||||
|
||||
self.set
|
||||
.raw_entry_mut()
|
||||
.from_hash(hash, |other| handle.value() == other.value())
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, handle: TaggedHandle) {
|
||||
if let hash_map::RawEntryMut::Vacant(entry) = self.entry_mut(handle) {
|
||||
let hash = handle.hash();
|
||||
entry.insert_with_hasher(hash, handle, (), |handle| handle.hash());
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, handle: TaggedHandle) {
|
||||
if let hash_map::RawEntryMut::Occupied(entry) = self.entry_mut(handle) {
|
||||
entry.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn contains(&self, handle: TaggedHandle) -> bool {
|
||||
let hash = handle.hash();
|
||||
|
||||
self.set
|
||||
.raw_entry()
|
||||
.from_hash(hash, |other| handle.value() == other.value())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = TaggedHandle> + '_ {
|
||||
self.set.keys().copied()
|
||||
}
|
||||
|
||||
pub fn difference<'a>(&'a self, other: &'a Self) -> impl Iterator<Item = TaggedHandle> + 'a {
|
||||
self.set
|
||||
.keys()
|
||||
.filter(|handle| !other.contains(**handle))
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.set.clear();
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
use super::{handle::TaggedHandle, set::ObjectSet};
|
||||
|
||||
pub trait Trace {
|
||||
fn visit(&self, visitor: &mut Visitor);
|
||||
}
|
||||
|
||||
pub struct Visitor {
|
||||
marked: ObjectSet,
|
||||
stale: Vec<TaggedHandle>,
|
||||
}
|
||||
|
||||
impl Visitor {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
marked: ObjectSet::new(),
|
||||
stale: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark(&mut self, handle: TaggedHandle) {
|
||||
self.marked.insert(handle);
|
||||
}
|
||||
|
||||
pub fn unmarked<'a>(
|
||||
&'a mut self,
|
||||
objects: &ObjectSet,
|
||||
) -> impl Iterator<Item = TaggedHandle> + 'a {
|
||||
self.stale.extend(objects.difference(&self.marked));
|
||||
self.stale.iter().copied()
|
||||
}
|
||||
|
||||
pub fn is_marked(&self, handle: TaggedHandle) -> bool {
|
||||
self.marked.contains(handle)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.marked.clear();
|
||||
self.stale.clear();
|
||||
}
|
||||
}
|
23
src/engine/gc2/internal/deck.rs
Normal file
23
src/engine/gc2/internal/deck.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use super::object::ObjectPtr;
|
||||
|
||||
pub struct Deck {
|
||||
tracked: HashSet<ObjectPtr>,
|
||||
}
|
||||
|
||||
impl Deck {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tracked: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track(&mut self, ptr: ObjectPtr) {
|
||||
self.tracked.insert(ptr);
|
||||
}
|
||||
|
||||
pub fn roots(&mut self) -> impl Iterator<Item = ObjectPtr> + '_ {
|
||||
self.tracked.drain()
|
||||
}
|
||||
}
|
143
src/engine/gc2/internal/eden.rs
Normal file
143
src/engine/gc2/internal/eden.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
use std::{
|
||||
alloc::{alloc, dealloc, Layout},
|
||||
collections::HashMap,
|
||||
mem,
|
||||
ptr,
|
||||
time,
|
||||
};
|
||||
|
||||
use super::{
|
||||
object::{ObjectHeader, ObjectPtr, ObjectType},
|
||||
EdenGCContext,
|
||||
};
|
||||
|
||||
const EDEN_INITIAL_SIZE: usize = 1024 * 64;
|
||||
|
||||
fn layout(size: usize) -> Layout {
|
||||
unsafe { Layout::from_size_align_unchecked(size, 1) }
|
||||
}
|
||||
|
||||
pub struct Eden {
|
||||
allocation_size: usize,
|
||||
limit: usize,
|
||||
count: usize,
|
||||
base: *mut u8,
|
||||
cursor: *mut u8,
|
||||
}
|
||||
|
||||
impl Eden {
|
||||
pub fn new() -> Self {
|
||||
let layout = layout(EDEN_INITIAL_SIZE);
|
||||
let base = unsafe { alloc(layout) };
|
||||
|
||||
Self {
|
||||
allocation_size: EDEN_INITIAL_SIZE,
|
||||
limit: EDEN_INITIAL_SIZE,
|
||||
count: 0,
|
||||
base,
|
||||
cursor: base,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn allocate(
|
||||
&mut self,
|
||||
ctx: &mut EdenGCContext,
|
||||
size: u32,
|
||||
ty: ObjectType,
|
||||
id: u32,
|
||||
) -> ObjectPtr {
|
||||
// Calculate total size.
|
||||
let alloc_start = self.cursor;
|
||||
let total_size = mem::size_of::<ObjectHeader>() + size as usize;
|
||||
|
||||
// Make sure we have enough space eden memory for this allocation
|
||||
let used = unsafe { self.cursor.sub(self.base as usize) as usize };
|
||||
if used + total_size <= self.limit {
|
||||
self.evacuate(ctx);
|
||||
}
|
||||
|
||||
// Advance the eden cursor
|
||||
self.cursor = unsafe { self.cursor.add(total_size) };
|
||||
|
||||
// Write the size of the allocation into the header
|
||||
let header = alloc_start as *mut ObjectHeader;
|
||||
unsafe { *header = ObjectHeader { ty, size, id } };
|
||||
|
||||
// Compute the payload pointer by skipping past the header
|
||||
let ptr = unsafe { alloc_start.add(mem::size_of::<ObjectHeader>()) };
|
||||
|
||||
self.count += 1;
|
||||
ObjectPtr::new(ptr)
|
||||
}
|
||||
|
||||
pub fn contains_object(&self, ptr: ObjectPtr) -> bool {
|
||||
let ptr = ptr.raw() as usize;
|
||||
ptr > self.base as usize || ptr < self.cursor as usize
|
||||
}
|
||||
|
||||
fn evacuate(&mut self, ctx: &mut EdenGCContext) {
|
||||
let mut relocated = HashMap::new();
|
||||
let relocate = &mut |src: ObjectPtr| {
|
||||
// Only evacuate objects inside of the eden region.
|
||||
if !self.contains_object(src) {
|
||||
return src;
|
||||
}
|
||||
|
||||
// Check if we've already relocated this object.
|
||||
if let Some(dst) = relocated.get(&src) {
|
||||
return *dst;
|
||||
}
|
||||
|
||||
// Relocate the object.
|
||||
unsafe {
|
||||
let header = &*src.header();
|
||||
let dst = ctx.old.allocate(header.size, header.ty, header.id);
|
||||
ptr::copy_nonoverlapping(src.raw(), dst.raw(), header.size as usize);
|
||||
relocated.insert(src, dst);
|
||||
dst
|
||||
}
|
||||
};
|
||||
|
||||
// Start execution timing.
|
||||
let start = time::Instant::now();
|
||||
|
||||
// Trace VM roots.
|
||||
(ctx.constraint)(relocate);
|
||||
|
||||
// Trace tracked objected in the old generation.
|
||||
for root in ctx.deck.roots() {
|
||||
unsafe {
|
||||
let object = &mut *root.virt();
|
||||
object.evacuate(relocate);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the timer.
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Calculate the new eden size.
|
||||
let eden_size = unsafe { self.cursor.sub(self.base as usize) as usize };
|
||||
let new_eden_size = ctx.pacer.step_eden(
|
||||
eden_size,
|
||||
ctx.old.size(),
|
||||
elapsed,
|
||||
self.count,
|
||||
relocated.len(),
|
||||
);
|
||||
|
||||
// Expand the eden region if needed.
|
||||
if new_eden_size > self.allocation_size {
|
||||
let new_allocation_size = new_eden_size.next_power_of_two();
|
||||
let old_layout = layout(self.allocation_size);
|
||||
unsafe { dealloc(self.base, old_layout) };
|
||||
let layout = layout(new_allocation_size);
|
||||
self.base = unsafe { alloc(layout) };
|
||||
self.allocation_size = new_allocation_size;
|
||||
}
|
||||
|
||||
// Reset allocator state.
|
||||
self.limit = new_eden_size;
|
||||
self.count = 0;
|
||||
self.cursor = self.base;
|
||||
}
|
||||
}
|
71
src/engine/gc2/internal/mod.rs
Normal file
71
src/engine/gc2/internal/mod.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
mod deck;
|
||||
mod eden;
|
||||
mod object;
|
||||
mod old;
|
||||
mod optimizer;
|
||||
mod pacer;
|
||||
|
||||
use std::{alloc::Layout, collections::HashSet, time::Duration};
|
||||
|
||||
use deck::Deck;
|
||||
use eden::Eden;
|
||||
pub use object::{Object, ObjectPtr, ObjectType};
|
||||
use old::Old;
|
||||
use pacer::Pacer;
|
||||
|
||||
const LARGE_OBJECT_THRESHOLD: usize = 4 * 1024;
|
||||
const MAX_PAUSE: Duration = Duration::from_millis(100);
|
||||
|
||||
pub type GCConstraint = dyn FnMut(&mut dyn FnMut(ObjectPtr) -> ObjectPtr);
|
||||
|
||||
pub struct InternalHeap {
|
||||
pacer: Pacer,
|
||||
eden: Eden,
|
||||
deck: Deck,
|
||||
old: Old,
|
||||
large_objects: HashSet<(*mut u8, Layout)>,
|
||||
constraint: Box<GCConstraint>,
|
||||
}
|
||||
|
||||
impl InternalHeap {
|
||||
pub fn new() -> InternalHeap {
|
||||
InternalHeap {
|
||||
pacer: Pacer::new(MAX_PAUSE),
|
||||
eden: Eden::new(),
|
||||
deck: Deck::new(),
|
||||
old: Old::new(),
|
||||
large_objects: HashSet::new(),
|
||||
constraint: Box::new(|_| ()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn allocate(&mut self, size: u32, ty: ObjectType) -> ObjectPtr {
|
||||
let mut ctx = EdenGCContext {
|
||||
pacer: &mut self.pacer,
|
||||
deck: &mut self.deck,
|
||||
old: &mut self.old,
|
||||
constraint: &mut self.constraint,
|
||||
};
|
||||
|
||||
self.eden.allocate(&mut ctx, size, ty, rand::random())
|
||||
}
|
||||
|
||||
pub fn barrier(&mut self, target: ObjectPtr, child: ObjectPtr) {
|
||||
if !self.eden.contains_object(target) || self.eden.contains_object(child) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.deck.track(target);
|
||||
}
|
||||
|
||||
pub fn set_constraint(&mut self, constraint: Box<GCConstraint>) {
|
||||
self.constraint = constraint;
|
||||
}
|
||||
}
|
||||
|
||||
struct EdenGCContext<'a> {
|
||||
pacer: &'a mut Pacer,
|
||||
deck: &'a mut Deck,
|
||||
old: &'a mut Old,
|
||||
constraint: &'a mut GCConstraint,
|
||||
}
|
78
src/engine/gc2/internal/object.rs
Normal file
78
src/engine/gc2/internal/object.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use std::mem;
|
||||
|
||||
use super::super::super::value::{ByteString, Function, Upvalue, Userdata};
|
||||
use crate::engine::value::Table2;
|
||||
|
||||
pub trait Object {
|
||||
fn evacuate(&mut self, visitor: &mut dyn FnMut(ObjectPtr) -> ObjectPtr);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ObjectHeader {
|
||||
pub size: u32,
|
||||
pub id: u32,
|
||||
pub ty: ObjectType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ObjectPtr {
|
||||
ptr: *mut u8,
|
||||
}
|
||||
|
||||
impl ObjectPtr {
|
||||
pub fn new(ptr: *mut u8) -> Self {
|
||||
Self { ptr }
|
||||
}
|
||||
|
||||
pub fn raw(self) -> *mut u8 {
|
||||
self.ptr
|
||||
}
|
||||
|
||||
pub fn header(self) -> *mut ObjectHeader {
|
||||
let offset = mem::size_of::<ObjectHeader>();
|
||||
unsafe { self.ptr.sub(offset) as *mut ObjectHeader }
|
||||
}
|
||||
|
||||
pub fn from<T>(ptr: *mut T) -> Self
|
||||
where
|
||||
T: Object,
|
||||
{
|
||||
ObjectPtr::new(ptr as *mut u8)
|
||||
}
|
||||
|
||||
pub fn cast<T>(self) -> *mut T
|
||||
where
|
||||
T: Object,
|
||||
{
|
||||
self.ptr as *mut T
|
||||
}
|
||||
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
pub fn from_header_ptr(header: *mut ObjectHeader) -> Self {
|
||||
let ptr = unsafe { header.add(1) as *mut u8 };
|
||||
Self { ptr }
|
||||
}
|
||||
|
||||
pub fn virt(self) -> *mut dyn Object {
|
||||
unsafe {
|
||||
let header = &*self.header();
|
||||
|
||||
match header.ty {
|
||||
ObjectType::String => self.cast::<ByteString>(),
|
||||
ObjectType::Userdata => self.cast::<Userdata>(),
|
||||
ObjectType::Function => self.cast::<Function>(),
|
||||
ObjectType::Table => self.cast::<Table2>(),
|
||||
ObjectType::Upvalue => self.cast::<Upvalue>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ObjectType {
|
||||
Table,
|
||||
String,
|
||||
Function,
|
||||
Userdata,
|
||||
Upvalue,
|
||||
}
|
41
src/engine/gc2/internal/old.rs
Normal file
41
src/engine/gc2/internal/old.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use std::{
|
||||
alloc::{alloc, Layout},
|
||||
collections::LinkedList,
|
||||
mem,
|
||||
};
|
||||
|
||||
use super::object::{ObjectHeader, ObjectPtr, ObjectType};
|
||||
|
||||
fn layout(size: usize) -> Layout {
|
||||
unsafe { Layout::from_size_align_unchecked(size, 1) }
|
||||
}
|
||||
|
||||
pub struct Old {
|
||||
objects: LinkedList<ObjectPtr>,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
impl Old {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
objects: LinkedList::new(),
|
||||
size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn allocate(&mut self, size: u32, ty: ObjectType, id: u32) -> ObjectPtr {
|
||||
let total_size = size as usize + mem::size_of::<ObjectHeader>();
|
||||
let layout = layout(total_size);
|
||||
|
||||
unsafe {
|
||||
let ptr = alloc(layout) as *mut ObjectHeader;
|
||||
(*ptr) = ObjectHeader { size, ty, id };
|
||||
|
||||
ObjectPtr::from_header_ptr(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.size
|
||||
}
|
||||
}
|
65
src/engine/gc2/internal/optimizer.rs
Normal file
65
src/engine/gc2/internal/optimizer.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
const LEARNING_RATE: f32 = 0.1;
|
||||
const MOMENTUM: f32 = 0.8;
|
||||
|
||||
/// [`ConvexOptimizer`] is an optimizer than finds a local minimum for an
|
||||
/// unknown convex function using an adaptive gradient descent algorithm.
|
||||
pub struct ConvexOptimizer {
|
||||
x: f32,
|
||||
prev_x: f32,
|
||||
prev_y: f32,
|
||||
prev_change: f32,
|
||||
}
|
||||
|
||||
impl ConvexOptimizer {
|
||||
/// Create a new optimizer with an initial guess of `x` and a threshold of
|
||||
/// when to stop optimization based on the change of `x`.
|
||||
pub fn new(x: f32) -> Self {
|
||||
Self {
|
||||
x,
|
||||
prev_x: 0.0,
|
||||
prev_y: 0.0,
|
||||
prev_change: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the absolute change in the function input since the last step.
|
||||
fn x_diff(&self) -> f32 {
|
||||
self.x - self.prev_x
|
||||
}
|
||||
|
||||
/// Approximate the derivative using the derivative definition.
|
||||
fn gradient(&self, y: f32) -> f32 {
|
||||
(y - self.prev_y) / (self.x_diff() + 1e-8)
|
||||
}
|
||||
|
||||
/// Calculate an absolute base change based on the approximate gradient.
|
||||
fn change(&self, y: f32) -> f32 {
|
||||
LEARNING_RATE * self.gradient(y)
|
||||
}
|
||||
|
||||
/// Accelerate the base change using previous momentum.
|
||||
fn accelerated_change(&self, y: f32) -> f32 {
|
||||
self.change(y) + MOMENTUM * self.prev_change
|
||||
}
|
||||
|
||||
/// Repredict the change using Nesterov's method.
|
||||
fn predicted_change(&self, y: f32) -> f32 {
|
||||
let change = self.accelerated_change(y);
|
||||
change + MOMENTUM * (change - self.prev_change)
|
||||
}
|
||||
|
||||
/// Step the optimizer forward by one iteration.
|
||||
/// Accepts the `y` value for the previous `x` value and yields a new `x`-value.
|
||||
pub fn step(&mut self, y: f32) -> f32 {
|
||||
// Compute the new change.
|
||||
let change = self.predicted_change(y);
|
||||
|
||||
// Update internal state with new data.
|
||||
self.prev_x = self.x;
|
||||
self.x -= change;
|
||||
self.prev_y = y;
|
||||
self.prev_change = change;
|
||||
|
||||
self.x
|
||||
}
|
||||
}
|
78
src/engine/gc2/internal/pacer.rs
Normal file
78
src/engine/gc2/internal/pacer.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use std::{cmp, time::Duration};
|
||||
|
||||
use super::optimizer::ConvexOptimizer;
|
||||
|
||||
const EDEN_SIZE_MINIMUM: usize = 1024 * 16;
|
||||
|
||||
fn smoothed(current: f32, new: f32) -> f32 {
|
||||
(current * 2.0 + new) / 3.0
|
||||
}
|
||||
|
||||
pub struct Pacer {
|
||||
/// `max_pause` is the maximum GC pause time that we should allow in
|
||||
/// seconds.
|
||||
max_pause: f32,
|
||||
|
||||
/// `evacuation_rate` is the evacuation speed from the eden region in bytes
|
||||
/// per second. This is stored in the form of an exponential moving
|
||||
/// average in order to smooth out various extreme values caused by the
|
||||
/// environment.
|
||||
evacuation_rate: f32,
|
||||
|
||||
/// `eden_optimizer` is a convex optimizer used to find the optimal eden
|
||||
/// size.
|
||||
eden_optimizer: ConvexOptimizer,
|
||||
}
|
||||
|
||||
impl Pacer {
|
||||
pub fn new(max_pause: Duration) -> Self {
|
||||
Self {
|
||||
max_pause: max_pause.as_secs_f32(),
|
||||
evacuation_rate: 0.0,
|
||||
eden_optimizer: ConvexOptimizer::new(EDEN_SIZE_MINIMUM as f32),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn adjust_max_pause(&mut self, max_pause: Duration) {
|
||||
self.max_pause = max_pause.as_secs_f32();
|
||||
}
|
||||
|
||||
/// `step_eden` collects eden evacuation metrics and recommends the new size
|
||||
/// of the eden region. The algorithm attempts to adapts to runtime
|
||||
/// conditions of the existing system by dynamically modifying internal
|
||||
/// tuning parameters based on previous collection metrics to minimize
|
||||
/// the runtime of the next collection and hit latency goals.
|
||||
pub fn step_eden(
|
||||
&mut self,
|
||||
eden_size: usize,
|
||||
heap_size: usize,
|
||||
elapsed: Duration,
|
||||
objects: usize,
|
||||
survivors: usize,
|
||||
) -> usize {
|
||||
// Calculate the evacauation rate. This is smoothed to prevent
|
||||
// fluctuations in the metrics from throwing off the maximum eden size
|
||||
// which could potentially cause us to miss latency goals during the next cycle.
|
||||
let observed_evacuation_rate = eden_size as f32 / elapsed.as_secs_f32();
|
||||
self.evacuation_rate = smoothed(self.evacuation_rate, observed_evacuation_rate);
|
||||
|
||||
// Measure the amount of work that was required for this collection and
|
||||
// step the optimizer to produce a new initial eden size.
|
||||
let observed_evacuation_survivors = survivors as f32 / objects as f32;
|
||||
let evacuation_work = observed_evacuation_survivors * eden_size as f32;
|
||||
let mut size = self.eden_optimizer.step(evacuation_work) as usize;
|
||||
|
||||
// Constrict size so that we always evacuate within the time limit.
|
||||
let latency = (self.max_pause * self.evacuation_rate * 0.9) as usize;
|
||||
size = cmp::min(size, latency);
|
||||
|
||||
// Limit eden size to a proportion of the heap size which is generally more
|
||||
// stable. This is done to prevent the eden size from growing extremely
|
||||
// large in comparison to heap which may be unexpected.
|
||||
let proportional = heap_size / 4;
|
||||
size = cmp::min(size, proportional);
|
||||
|
||||
// Bound the eden size to a minimum.
|
||||
cmp::max(size, EDEN_SIZE_MINIMUM)
|
||||
}
|
||||
}
|
76
src/engine/gc2/mod.rs
Normal file
76
src/engine/gc2/mod.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
mod internal;
|
||||
|
||||
use std::{cell::RefCell, ptr};
|
||||
|
||||
use internal::InternalHeap;
|
||||
pub use internal::{Object, ObjectPtr, ObjectType};
|
||||
|
||||
use super::value::{ByteString, Function, Table2, Upvalue};
|
||||
|
||||
pub struct Heap {
|
||||
internal: RefCell<InternalHeap>,
|
||||
}
|
||||
|
||||
impl Heap {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Heap {
|
||||
Heap {
|
||||
internal: RefCell::new(InternalHeap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate(&self, size: u32, ty: ObjectType) -> ObjectPtr {
|
||||
self.internal.borrow_mut().allocate(size, ty)
|
||||
}
|
||||
|
||||
pub fn allocate_string(&self, bytes: &[u8]) -> ObjectPtr {
|
||||
let len = bytes.len() as u32;
|
||||
let layout = ByteString::layout(len);
|
||||
|
||||
unsafe {
|
||||
let ptr = self.allocate(layout.size() as u32, ObjectType::String);
|
||||
let raw = ptr.cast::<ByteString>();
|
||||
ByteString::initialize_into(raw, len);
|
||||
ptr::copy_nonoverlapping(bytes.as_ptr(), (&mut *raw).offset(0), len as usize);
|
||||
ptr
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate_table(&self, capacity: u32) -> ObjectPtr {
|
||||
let layout = Table2::layout(capacity);
|
||||
|
||||
unsafe {
|
||||
let ptr = self.allocate(layout.size() as u32, ObjectType::Table);
|
||||
let raw = ptr.cast::<Table2>();
|
||||
Table2::initialize_into(raw, capacity);
|
||||
ptr
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate_function(
|
||||
&self,
|
||||
chunk: u32,
|
||||
upvalue_len: u32,
|
||||
upvalues: &mut dyn Iterator<Item = ObjectPtr>,
|
||||
) -> ObjectPtr {
|
||||
let layout = Function::layout(upvalue_len);
|
||||
|
||||
unsafe {
|
||||
let ptr = self.allocate(layout.size() as u32, ObjectType::Function);
|
||||
let raw = ptr.cast::<Function>();
|
||||
Function::initialize_into(raw, chunk, upvalues);
|
||||
ptr
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn allocate_upvalue(&self, location: usize) -> ObjectPtr {
|
||||
let layout = Upvalue::layout();
|
||||
|
||||
unsafe {
|
||||
let ptr = self.allocate(layout.size() as u32, ObjectType::Upvalue);
|
||||
let raw = ptr.cast::<Upvalue>();
|
||||
Upvalue::initialize_into(raw, location);
|
||||
ptr
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
mod bytecode;
|
||||
mod error;
|
||||
pub mod gc;
|
||||
pub mod gc2;
|
||||
mod util;
|
||||
pub mod value;
|
||||
pub mod vm;
|
||||
|
|
|
@ -25,7 +25,7 @@ pub fn is_nil(x: u64) -> bool {
|
|||
x == NIL_VALUE
|
||||
}
|
||||
|
||||
pub fn make_nil() -> u64 {
|
||||
pub const fn make_nil() -> u64 {
|
||||
NIL_VALUE
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1,86 @@
|
|||
pub struct Function {}
|
||||
use std::alloc;
|
||||
|
||||
use super::{
|
||||
super::gc2::{Object, ObjectPtr},
|
||||
Value,
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Function {
|
||||
chunk: u32,
|
||||
upvalues: [ObjectPtr; 0],
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub(crate) unsafe fn initialize_into(
|
||||
ptr: *mut Self,
|
||||
chunk: u32,
|
||||
upvalues: &mut dyn Iterator<Item = ObjectPtr>,
|
||||
) {
|
||||
(*ptr).chunk = chunk;
|
||||
|
||||
let base = (&mut (*ptr).upvalues) as *mut ObjectPtr;
|
||||
for (i, upvalue) in upvalues.enumerate() {
|
||||
base.add(i).write(upvalue);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout(upvalues: u32) -> alloc::Layout {
|
||||
let size =
|
||||
std::mem::size_of::<u32>() + upvalues as usize * std::mem::size_of::<ObjectPtr>();
|
||||
let align = std::mem::align_of::<ObjectPtr>();
|
||||
alloc::Layout::from_size_align(size, align).unwrap()
|
||||
}
|
||||
|
||||
pub fn chunk(&self) -> u32 {
|
||||
self.chunk
|
||||
}
|
||||
|
||||
pub fn get_upvalue(&self, stack: &mut [Value], key: u8) -> Value {
|
||||
unsafe {
|
||||
let base = (&self.upvalues) as *const ObjectPtr;
|
||||
let obj = &*(*base.add(key as usize)).cast::<Upvalue>();
|
||||
obj.load(stack)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_upvalue(&mut self, stack: &mut [Value], key: u8, value: Value) {
|
||||
unsafe {
|
||||
let base = (&mut self.upvalues) as *mut ObjectPtr;
|
||||
let obj = &*(*base.add(key as usize)).cast::<Upvalue>();
|
||||
obj.set(stack, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Object for Function {
|
||||
fn evacuate(&mut self, _visitor: &mut dyn FnMut(ObjectPtr) -> ObjectPtr) {}
|
||||
}
|
||||
|
||||
pub struct Upvalue {
|
||||
location: usize,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
impl Upvalue {
|
||||
pub(crate) unsafe fn initialize_into(ptr: *mut Self, location: usize) {
|
||||
(*ptr).location = location;
|
||||
(*ptr).value = Value::nil();
|
||||
}
|
||||
|
||||
pub fn layout() -> alloc::Layout {
|
||||
alloc::Layout::new::<Self>()
|
||||
}
|
||||
|
||||
pub fn load(&self, stack: &mut [Value]) -> Value {
|
||||
stack[self.location]
|
||||
}
|
||||
|
||||
pub fn set(&self, stack: &mut [Value], value: Value) {
|
||||
stack[self.location] = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl Object for Upvalue {
|
||||
fn evacuate(&mut self, _visitor: &mut dyn FnMut(ObjectPtr) -> ObjectPtr) {}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
pub mod encoding;
|
||||
mod function;
|
||||
mod string;
|
||||
mod table;
|
||||
mod table2;
|
||||
mod userdata;
|
||||
|
||||
use std::cmp::PartialEq;
|
||||
|
||||
use encoding::*;
|
||||
pub use function::Function;
|
||||
pub use function::{Function, Upvalue};
|
||||
pub use string::ByteString;
|
||||
pub use table::Table;
|
||||
pub use table2::Table2;
|
||||
pub use userdata::Userdata;
|
||||
|
||||
use super::{
|
||||
gc::{Handle, TaggedHandle, Trace, Visitor},
|
||||
util::mix_u64,
|
||||
vm::ctx::Ctx,
|
||||
};
|
||||
use super::{gc2::ObjectPtr, Error};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ValueType {
|
||||
|
@ -46,30 +42,29 @@ macro_rules! dispatch {
|
|||
match $x {
|
||||
$(v if $guard(v) => $arm),*,
|
||||
#[allow(unused_unsafe)]
|
||||
_ => unsafe {
|
||||
#[cfg(debug_assertions)]
|
||||
unreachable!();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
std::hint::unreachable_unchecked();
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
fn int_op(iop: fn(i32, i32) -> i32, a: Value, b: Value) -> Value {
|
||||
fn int_op(iop: fn(i32, i32) -> i32, a: Value, b: Value) -> Result<Value, Error> {
|
||||
if a.ty() != ValueType::Int || b.ty() != ValueType::Int {
|
||||
panic!("int_op: invalid types");
|
||||
return Err(Error::new("int_op: invalid types"));
|
||||
}
|
||||
|
||||
Value::from_int(iop(get_int(a.data), get_int(b.data)))
|
||||
Ok(Value::from_int(iop(get_int(a.data), get_int(b.data))))
|
||||
}
|
||||
|
||||
fn arith_op(iop: fn(i32, i32) -> Value, fop: fn(f64, f64) -> Value, a: Value, b: Value) -> Value {
|
||||
fn arith_op(
|
||||
iop: fn(i32, i32) -> Value,
|
||||
fop: fn(f64, f64) -> Value,
|
||||
a: Value,
|
||||
b: Value,
|
||||
) -> Result<Value, Error> {
|
||||
if a.ty() == ValueType::Float || b.ty() == ValueType::Float {
|
||||
fop(a.convert_float(), b.convert_float())
|
||||
Ok(fop(a.convert_float()?, b.convert_float()?))
|
||||
} else {
|
||||
iop(a.cast_int(), b.cast_int())
|
||||
Ok(iop(a.cast_int()?, b.cast_int()?))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,7 +89,7 @@ pub struct Value {
|
|||
}
|
||||
|
||||
impl Value {
|
||||
pub fn from_nil() -> Self {
|
||||
pub const fn nil() -> Self {
|
||||
Value { data: make_nil() }
|
||||
}
|
||||
|
||||
|
@ -112,21 +107,21 @@ impl Value {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_table(x: Handle<Table>) -> Self {
|
||||
pub fn from_table2(x: ObjectPtr) -> Self {
|
||||
Value {
|
||||
data: make_table(x.as_ptr() as *mut u8),
|
||||
data: make_table(x.raw()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(x: Handle<ByteString>) -> Self {
|
||||
pub fn from_string(x: ObjectPtr) -> Self {
|
||||
Value {
|
||||
data: make_string(x.as_ptr() as *mut u8),
|
||||
data: make_string(x.raw()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_function(x: *mut u8) -> Self {
|
||||
pub fn from_function(x: ObjectPtr) -> Self {
|
||||
Value {
|
||||
data: make_function(x),
|
||||
data: make_function(x.raw()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,8 +139,24 @@ impl Value {
|
|||
get_bool(self.data)
|
||||
}
|
||||
|
||||
fn cast_table_unchecked<'a>(self) -> &'a Table {
|
||||
unsafe { &*(get_table(self.data) as *const Table) }
|
||||
pub fn cast_mut_table2<'a>(self) -> Option<&'a mut Table2> {
|
||||
if self.ty() != ValueType::Table {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(unsafe { &mut *(get_table(self.data) as *mut Table2) })
|
||||
}
|
||||
|
||||
pub fn cast_table_unchecked<'a>(self) -> &'a mut Table2 {
|
||||
unsafe { &mut *(get_table(self.data) as *mut Table2) }
|
||||
}
|
||||
|
||||
pub fn cast_function(self) -> Option<ObjectPtr> {
|
||||
if self.ty() != ValueType::Function {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ObjectPtr::from(get_function(self.data) as *mut Function))
|
||||
}
|
||||
|
||||
pub fn is_truthy(self) -> bool {
|
||||
|
@ -156,19 +167,19 @@ impl Value {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn convert_float(self) -> f64 {
|
||||
match self.ty() {
|
||||
pub fn convert_float(self) -> Result<f64, Error> {
|
||||
Ok(match self.ty() {
|
||||
ValueType::Float => get_float(self.data),
|
||||
ValueType::Int => get_int(self.data) as f64,
|
||||
_ => panic!("cannot convert to float"),
|
||||
}
|
||||
_ => return Err(Error::new("cannot convert to float")),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cast_int(self) -> i32 {
|
||||
match self.ty() {
|
||||
pub fn cast_int(self) -> Result<i32, Error> {
|
||||
Ok(match self.ty() {
|
||||
ValueType::Int => get_int(self.data),
|
||||
_ => panic!("value is not int"),
|
||||
}
|
||||
_ => return Err(Error::new("value is not int")),
|
||||
})
|
||||
}
|
||||
|
||||
fn ty(self) -> ValueType {
|
||||
|
@ -188,36 +199,44 @@ impl Value {
|
|||
Value::from_bool(self.data == other.data)
|
||||
}
|
||||
|
||||
pub fn op_gt(self, other: Self) -> Value {
|
||||
pub fn op_gt(self, other: Self) -> Result<Value, Error> {
|
||||
let ty_1 = self.ty();
|
||||
let ty_2 = other.ty();
|
||||
|
||||
if ty_1 != ty_2 {
|
||||
return Value::from_bool(false);
|
||||
return Ok(Value::from_bool(false));
|
||||
}
|
||||
|
||||
Value::from_bool(match ty_1 {
|
||||
Ok(Value::from_bool(match ty_1 {
|
||||
ValueType::Int => get_int(self.data) > get_int(other.data),
|
||||
ValueType::Float => get_float(self.data) > get_float(other.data),
|
||||
ValueType::String => **self.cast_string_unchecked() > **other.cast_string_unchecked(),
|
||||
_ => panic!("attempted op_gt on unsupported type: {:?}", ty_1),
|
||||
})
|
||||
_ =>
|
||||
return Err(Error::new(&format!(
|
||||
"attempted op_gt on unsupported type: {:?}",
|
||||
ty_1
|
||||
))),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn op_lt(self, other: Self) -> Value {
|
||||
pub fn op_lt(self, other: Self) -> Result<Value, Error> {
|
||||
let ty_1 = self.ty();
|
||||
let ty_2 = other.ty();
|
||||
|
||||
if ty_1 != ty_2 {
|
||||
return Value::from_bool(false);
|
||||
return Ok(Value::from_bool(false));
|
||||
}
|
||||
|
||||
Value::from_bool(match ty_1 {
|
||||
Ok(Value::from_bool(match ty_1 {
|
||||
ValueType::Int => get_int(self.data) < get_int(other.data),
|
||||
ValueType::Float => get_float(self.data) < get_float(other.data),
|
||||
ValueType::String => **self.cast_string_unchecked() < **other.cast_string_unchecked(),
|
||||
_ => panic!("attempted op_lt on unsupported type: {:?}", ty_1),
|
||||
})
|
||||
_ =>
|
||||
return Err(Error::new(&format!(
|
||||
"attempted op_lt on unsupported type: {:?}",
|
||||
ty_1
|
||||
))),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn op_and(self, other: Self) -> Self {
|
||||
|
@ -228,7 +247,7 @@ impl Value {
|
|||
Value::from_bool(self.is_truthy() || other.is_truthy())
|
||||
}
|
||||
|
||||
pub fn op_add(self, other: Self) -> Self {
|
||||
pub fn op_add(self, other: Self) -> Result<Self, Error> {
|
||||
arith_op(
|
||||
|a, b| Value::from_int(a + b),
|
||||
|a, b| Value::from_float(a + b),
|
||||
|
@ -237,7 +256,7 @@ impl Value {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn op_sub(self, other: Self) -> Self {
|
||||
pub fn op_sub(self, other: Self) -> Result<Self, Error> {
|
||||
arith_op(
|
||||
|a, b| Value::from_int(a - b),
|
||||
|a, b| Value::from_float(a - b),
|
||||
|
@ -246,7 +265,7 @@ impl Value {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn op_mul(self, other: Self) -> Self {
|
||||
pub fn op_mul(self, other: Self) -> Result<Self, Error> {
|
||||
arith_op(
|
||||
|a, b| Value::from_int(a * b),
|
||||
|a, b| Value::from_float(a * b),
|
||||
|
@ -255,7 +274,7 @@ impl Value {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn op_div(self, other: Self) -> Self {
|
||||
pub fn op_div(self, other: Self) -> Result<Self, Error> {
|
||||
arith_op(
|
||||
|a, b| Value::from_float(a as f64 / b as f64),
|
||||
|a, b| Value::from_float(a / b),
|
||||
|
@ -264,7 +283,7 @@ impl Value {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn op_int_div(self, other: Self) -> Self {
|
||||
pub fn op_int_div(self, other: Self) -> Result<Self, Error> {
|
||||
arith_op(
|
||||
|a, b| Value::from_int(a / b),
|
||||
|a, b| Value::from_int((a / b).floor() as i32),
|
||||
|
@ -273,7 +292,7 @@ impl Value {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn op_exp(self, other: Self) -> Self {
|
||||
pub fn op_exp(self, other: Self) -> Result<Self, Error> {
|
||||
arith_op(
|
||||
|a, b| Value::from_int(a.pow(b as u32)),
|
||||
|a, b| Value::from_float(a.powf(b)),
|
||||
|
@ -282,7 +301,7 @@ impl Value {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn op_mod(self, other: Self) -> Self {
|
||||
pub fn op_mod(self, other: Self) -> Result<Self, Error> {
|
||||
arith_op(
|
||||
|a, b| Value::from_int(a % b),
|
||||
|a, b| Value::from_float(a % b),
|
||||
|
@ -291,23 +310,23 @@ impl Value {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn op_bit_and(self, other: Self) -> Self {
|
||||
pub fn op_bit_and(self, other: Self) -> Result<Self, Error> {
|
||||
int_op(|a, b| a & b, self, other)
|
||||
}
|
||||
|
||||
pub fn op_bit_or(self, other: Self) -> Self {
|
||||
pub fn op_bit_or(self, other: Self) -> Result<Self, Error> {
|
||||
int_op(|a, b| a | b, self, other)
|
||||
}
|
||||
|
||||
pub fn op_lshift(self, other: Self) -> Self {
|
||||
pub fn op_lshift(self, other: Self) -> Result<Self, Error> {
|
||||
int_op(|a, b| a << b, self, other)
|
||||
}
|
||||
|
||||
pub fn op_rshift(self, other: Self) -> Self {
|
||||
pub fn op_rshift(self, other: Self) -> Result<Self, Error> {
|
||||
int_op(|a, b| a >> b, self, other)
|
||||
}
|
||||
|
||||
pub fn op_bit_xor(self, other: Self) -> Self {
|
||||
pub fn op_bit_xor(self, other: Self) -> Result<Self, Error> {
|
||||
int_op(|a, b| a ^ b, self, other)
|
||||
}
|
||||
|
||||
|
@ -315,68 +334,75 @@ impl Value {
|
|||
Value::from_bool(!get_bool(self.op_eq(other).data))
|
||||
}
|
||||
|
||||
pub fn op_leq(self, other: Self) -> Self {
|
||||
pub fn op_leq(self, other: Self) -> Result<Self, Error> {
|
||||
let ty_1 = self.ty();
|
||||
let ty_2 = other.ty();
|
||||
|
||||
if ty_1 != ty_2 {
|
||||
return Value::from_bool(false);
|
||||
return Ok(Value::from_bool(false));
|
||||
}
|
||||
|
||||
Value::from_bool(match ty_1 {
|
||||
Ok(Value::from_bool(match ty_1 {
|
||||
ValueType::Int => get_int(self.data) <= get_int(other.data),
|
||||
ValueType::Float => get_float(self.data) <= get_float(other.data),
|
||||
ValueType::String => **self.cast_string_unchecked() <= **other.cast_string_unchecked(),
|
||||
_ => panic!("attempted op_leq on unsupported type: {:?}", ty_1),
|
||||
})
|
||||
_ =>
|
||||
return Err(Error::new(&format!(
|
||||
"attempted op_leq on unsupported type: {:?}",
|
||||
ty_1
|
||||
))),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn op_geq(self, other: Self) -> Self {
|
||||
pub fn op_geq(self, other: Self) -> Result<Self, Error> {
|
||||
let ty_1 = self.ty();
|
||||
let ty_2 = other.ty();
|
||||
|
||||
if ty_1 != ty_2 {
|
||||
return Value::from_bool(false);
|
||||
return Ok(Value::from_bool(false));
|
||||
}
|
||||
|
||||
Value::from_bool(match ty_1 {
|
||||
Ok(Value::from_bool(match ty_1 {
|
||||
ValueType::Int => get_int(self.data) >= get_int(other.data),
|
||||
ValueType::Float => get_float(self.data) >= get_float(other.data),
|
||||
ValueType::String => **self.cast_string_unchecked() >= **other.cast_string_unchecked(),
|
||||
_ => panic!("attempted op_geq on unsupported type: {:?}", ty_1),
|
||||
})
|
||||
_ =>
|
||||
return Err(Error::new(&format!(
|
||||
"attempted op_geq on unsupported type: {:?}",
|
||||
ty_1
|
||||
))),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn op_property(self, other: Self) -> Self {
|
||||
pub fn op_property(self, other: Self) -> Result<Self, Error> {
|
||||
if self.ty() != ValueType::Table {
|
||||
panic!()
|
||||
return Err(Error::new("attempted op_property on non-table"));
|
||||
}
|
||||
|
||||
let table = self.cast_table_unchecked();
|
||||
table.get(other)
|
||||
Ok(table.get(other))
|
||||
}
|
||||
|
||||
pub fn op_method(self, other: Self) -> Self {
|
||||
pub fn op_method(self, other: Self) -> Result<Self, Error> {
|
||||
if self.ty() != ValueType::Table {
|
||||
panic!()
|
||||
return Err(Error::new("attempted op_method on non-table"));
|
||||
}
|
||||
|
||||
let table = self.cast_table_unchecked();
|
||||
let value = table.get(other);
|
||||
|
||||
if value.ty() != ValueType::Function {
|
||||
panic!()
|
||||
}
|
||||
|
||||
value
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn op_concat(self, other: Self, ctx: &Ctx) -> Self {
|
||||
pub fn op_concat(
|
||||
self,
|
||||
other: Self,
|
||||
alloc: &mut dyn FnMut(&[u8]) -> ObjectPtr,
|
||||
) -> Result<Self, Error> {
|
||||
let ty_1 = self.ty();
|
||||
let ty_2 = other.ty();
|
||||
|
||||
if ty_1 != ValueType::String && ty_2 != ValueType::String {
|
||||
panic!()
|
||||
return Err(Error::new("attempted op_concat on non-string"));
|
||||
}
|
||||
|
||||
let str_1 = self.cast_string_unchecked();
|
||||
|
@ -384,24 +410,23 @@ impl Value {
|
|||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(str_1);
|
||||
buf.extend_from_slice(str_2);
|
||||
let new_str = ctx.intern(&buf);
|
||||
Value::from_string(new_str)
|
||||
Ok(Value::from_string(alloc(&buf)))
|
||||
}
|
||||
|
||||
pub fn op_neg(self) -> Self {
|
||||
match self.ty() {
|
||||
pub fn op_neg(self) -> Result<Self, Error> {
|
||||
Ok(match self.ty() {
|
||||
ValueType::Int => Value::from_int(-get_int(self.data)),
|
||||
ValueType::Float => Value::from_float(-get_float(self.data)),
|
||||
_ => panic!(),
|
||||
}
|
||||
_ => return Err(Error::new("attempted op_neg on unsupported type")),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn op_not(self) -> Self {
|
||||
Value::from_bool(!self.is_truthy())
|
||||
}
|
||||
|
||||
pub fn op_len(self) -> Self {
|
||||
match self.ty() {
|
||||
pub fn op_len(self) -> Result<Self, Error> {
|
||||
Ok(match self.ty() {
|
||||
ValueType::Table => {
|
||||
let len = self.cast_table_unchecked().len();
|
||||
Value::from_int(len as i32)
|
||||
|
@ -410,35 +435,21 @@ impl Value {
|
|||
let len = self.cast_string_unchecked().len();
|
||||
Value::from_int(len as i32)
|
||||
},
|
||||
_ => panic!(),
|
||||
}
|
||||
_ => return Err(Error::new("attempted op_len on unsupported type")),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn op_bit_not(self) -> Self {
|
||||
pub fn op_bit_not(self) -> Result<Self, Error> {
|
||||
if let ValueType::Int = self.ty() {
|
||||
let x = get_int(self.data);
|
||||
return Value::from_int(!x);
|
||||
return Ok(Value::from_int(!x));
|
||||
}
|
||||
|
||||
panic!()
|
||||
Err(Error::new("attempted op_bit_not on unsupported type"))
|
||||
}
|
||||
|
||||
pub fn op_hash(self) -> u64 {
|
||||
mix_u64(self.data)
|
||||
}
|
||||
}
|
||||
|
||||
impl Trace for Value {
|
||||
fn visit(&self, visitor: &mut Visitor) {
|
||||
let handle = TaggedHandle::new(self.data);
|
||||
|
||||
if is_ptr(self.data) && !visitor.is_marked(handle) {
|
||||
visitor.mark(handle);
|
||||
|
||||
if is_table(self.data) {
|
||||
let table = unsafe { &mut *(get_table(self.data) as *mut Table) };
|
||||
table.visit(visitor);
|
||||
}
|
||||
}
|
||||
todo!()
|
||||
// mix_u64(self.data)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::{alloc, mem::MaybeUninit, ops::Deref};
|
||||
use std::{alloc, mem::MaybeUninit, ops::Deref, ptr::addr_of};
|
||||
|
||||
use super::{super::gc::PtrTag, encoding};
|
||||
use super::super::gc2::{Object, ObjectPtr};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ByteString {
|
||||
|
@ -9,9 +9,7 @@ pub struct ByteString {
|
|||
}
|
||||
|
||||
impl ByteString {
|
||||
/// # Safety
|
||||
/// - ptr must point to an uninitialized ByteString.
|
||||
pub unsafe fn initialize_into(ptr: *mut Self, len: u32) {
|
||||
pub(crate) unsafe fn initialize_into(ptr: *mut Self, len: u32) {
|
||||
(*ptr).len = len;
|
||||
}
|
||||
|
||||
|
@ -24,6 +22,13 @@ impl ByteString {
|
|||
let align = std::mem::align_of::<u32>();
|
||||
alloc::Layout::from_size_align(size, align).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn data<'a>(ptr: *mut Self) -> &'a [u8] {
|
||||
unsafe {
|
||||
let addr = addr_of!((*ptr).data) as *const u8;
|
||||
std::slice::from_raw_parts(addr, (*ptr).len as usize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ByteString {
|
||||
|
@ -38,12 +43,6 @@ impl Deref for ByteString {
|
|||
}
|
||||
}
|
||||
|
||||
unsafe impl PtrTag for ByteString {
|
||||
fn is(x: u64) -> bool {
|
||||
encoding::is_string(x)
|
||||
}
|
||||
|
||||
fn tag(x: usize) -> u64 {
|
||||
encoding::make_string(x as *mut u8)
|
||||
}
|
||||
impl Object for ByteString {
|
||||
fn evacuate(&mut self, _visitor: &mut dyn FnMut(ObjectPtr) -> ObjectPtr) {}
|
||||
}
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
//! TODO(#29): Replace this with a butterfly-like structure.
|
||||
|
||||
use hashbrown::{hash_map, HashMap};
|
||||
|
||||
use super::{
|
||||
super::gc::{Heap, PtrTag, Trace, Visitor},
|
||||
encoding,
|
||||
Value,
|
||||
};
|
||||
|
||||
pub struct Table {
|
||||
map: HashMap<Value, Value, (), Heap>,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
pub fn new(heap: Heap) -> Self {
|
||||
Table {
|
||||
map: HashMap::with_hasher_in((), heap),
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_mut(&mut self, key: Value) -> hash_map::RawEntryMut<'_, Value, Value, (), Heap> {
|
||||
let hash = key.op_hash();
|
||||
|
||||
self.map
|
||||
.raw_entry_mut()
|
||||
.from_hash(hash, |other| key.op_eq(*other).cast_bool_unchecked())
|
||||
}
|
||||
|
||||
pub fn get(&self, key: Value) -> Value {
|
||||
let hash = key.op_hash();
|
||||
|
||||
self.map
|
||||
.raw_entry()
|
||||
.from_hash(hash, |other| key.op_eq(*other).cast_bool_unchecked())
|
||||
.map(|(_, v)| v)
|
||||
.copied()
|
||||
.unwrap_or_else(Value::from_nil)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: Value, value: Value) {
|
||||
match self.entry_mut(key) {
|
||||
hash_map::RawEntryMut::Vacant(entry) => {
|
||||
let hash = key.op_hash();
|
||||
entry.insert_with_hasher(hash, key, value, |key| key.op_hash());
|
||||
},
|
||||
|
||||
hash_map::RawEntryMut::Occupied(mut entry) => {
|
||||
*entry.get_mut() = value;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: Value) {
|
||||
if let hash_map::RawEntryMut::Occupied(entry) = self.entry_mut(key) {
|
||||
entry.remove();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.map.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Trace for Table {
|
||||
fn visit(&self, visitor: &mut Visitor) {
|
||||
self.map.iter().for_each(|(key, value)| {
|
||||
key.visit(visitor);
|
||||
value.visit(visitor);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl PtrTag for Table {
|
||||
fn is(x: u64) -> bool {
|
||||
encoding::is_table(x)
|
||||
}
|
||||
|
||||
fn tag(x: usize) -> u64 {
|
||||
encoding::make_table(x as *mut u8)
|
||||
}
|
||||
}
|
94
src/engine/value/table2.rs
Normal file
94
src/engine/value/table2.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use core::ptr::addr_of_mut;
|
||||
use std::{alloc, mem::MaybeUninit, ptr};
|
||||
|
||||
use super::{
|
||||
super::gc2::{Object, ObjectPtr},
|
||||
Value,
|
||||
};
|
||||
|
||||
const LOAD_FACTOR_THRESHOLD: f32 = 0.75;
|
||||
|
||||
const EMPTY_SLOT: Slot = Slot {
|
||||
key: Value::nil(),
|
||||
value: Value::nil(),
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Slot {
|
||||
key: Value,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
macro_rules! slots {
|
||||
($t:expr) => {{
|
||||
let addr = addr_of_mut!((*$t).data) as *mut Slot;
|
||||
&mut *ptr::slice_from_raw_parts_mut(addr, (*$t).capacity as usize)
|
||||
}};
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Table2 {
|
||||
capacity: u32,
|
||||
len: u32,
|
||||
data: [MaybeUninit<Slot>; 0],
|
||||
}
|
||||
|
||||
impl Table2 {
|
||||
pub(crate) unsafe fn initialize_into(ptr: *mut Self, capacity: u32) {
|
||||
(*ptr).capacity = capacity;
|
||||
(*ptr).len = 0;
|
||||
|
||||
for slot in slots!(ptr) {
|
||||
*slot = EMPTY_SLOT;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout(capacity: u32) -> alloc::Layout {
|
||||
let size = std::mem::size_of::<Table2>() + capacity as usize * std::mem::size_of::<Slot>();
|
||||
let align = std::mem::align_of::<Slot>();
|
||||
alloc::Layout::from_size_align(size, align).unwrap()
|
||||
}
|
||||
|
||||
fn load_factor(&self) -> f32 {
|
||||
self.len as f32 / self.capacity as f32
|
||||
}
|
||||
|
||||
fn grow(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// TODO: handle deletions
|
||||
pub fn set(&mut self, key: Value, value: Value) {
|
||||
let mut idx = key.op_hash() as usize;
|
||||
let slots = unsafe { slots!(self) };
|
||||
|
||||
loop {
|
||||
idx %= self.capacity as usize;
|
||||
let slot = &mut slots[idx];
|
||||
if slot.key == Value::nil() || slot.key == key {
|
||||
slot.key = key;
|
||||
slot.value = value;
|
||||
self.len += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
if self.load_factor() > LOAD_FACTOR_THRESHOLD {
|
||||
self.grow();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, _key: Value) -> Value {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Object for Table2 {
|
||||
fn evacuate(&mut self, _visitor: &mut dyn FnMut(ObjectPtr) -> ObjectPtr) {}
|
||||
}
|
|
@ -1 +1,7 @@
|
|||
use super::super::gc2::{Object, ObjectPtr};
|
||||
|
||||
pub struct Userdata {}
|
||||
|
||||
impl Object for Userdata {
|
||||
fn evacuate(&mut self, _visitor: &mut dyn FnMut(ObjectPtr) -> ObjectPtr) {}
|
||||
}
|
||||
|
|
308
src/engine/vm.rs
Normal file
308
src/engine/vm.rs
Normal file
|
@ -0,0 +1,308 @@
|
|||
use std::{
|
||||
collections::{hash_map, HashMap, LinkedList},
|
||||
hash::{BuildHasher, Hash, Hasher},
|
||||
};
|
||||
|
||||
use super::{
|
||||
bytecode::Instruction,
|
||||
gc2::{Heap, ObjectPtr},
|
||||
value::{ByteString, Table2, Value},
|
||||
Error,
|
||||
};
|
||||
use crate::engine::value::Function;
|
||||
|
||||
fn intern(heap: &Heap, strings: &mut HashMap<ObjectPtr, ()>, buf: &[u8]) -> ObjectPtr {
|
||||
let mut hasher = strings.hasher().build_hasher();
|
||||
buf.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
let compare = |other: &ObjectPtr| buf == ByteString::data(other.cast::<ByteString>());
|
||||
|
||||
let raw = strings.raw_entry_mut().from_hash(hash, compare);
|
||||
|
||||
match raw {
|
||||
hash_map::RawEntryMut::Occupied(entry) => *entry.key(),
|
||||
hash_map::RawEntryMut::Vacant(entry) => {
|
||||
let ptr = heap.allocate_string(buf);
|
||||
entry.insert_hashed_nocheck(hash, ptr, ());
|
||||
ptr
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VM {
|
||||
heap: Heap,
|
||||
global: ObjectPtr,
|
||||
strings: HashMap<ObjectPtr, ()>,
|
||||
chunks: HashMap<u32, Chunk>,
|
||||
callstack: Vec<CallFrame>,
|
||||
acall: LinkedList<Value>,
|
||||
registers: Vec<Value>,
|
||||
}
|
||||
|
||||
impl VM {
|
||||
pub(crate) fn new() -> Self {
|
||||
let heap = Heap::new();
|
||||
let global = heap.allocate_table(0);
|
||||
|
||||
VM {
|
||||
heap,
|
||||
global,
|
||||
strings: HashMap::new(),
|
||||
chunks: HashMap::new(),
|
||||
callstack: Vec::new(),
|
||||
acall: LinkedList::new(),
|
||||
registers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self, function: ObjectPtr) -> Result<Value, Error> {
|
||||
let frame = precall(&mut self.registers, function, &self.chunks, 0, 0);
|
||||
self.callstack.push(frame);
|
||||
self.execute_pop()
|
||||
}
|
||||
|
||||
fn execute_pop(&mut self) -> Result<Value, Error> {
|
||||
'call: loop {
|
||||
if self.callstack.is_empty() {
|
||||
return Ok(Value::nil());
|
||||
}
|
||||
|
||||
let frame = self.callstack.last_mut().unwrap();
|
||||
let chunk_id = unsafe { (*frame.function.cast::<Function>()).chunk() };
|
||||
let chunk = &self.chunks[&chunk_id];
|
||||
|
||||
macro_rules! r_reg {
|
||||
($reg:expr) => {{
|
||||
&mut self.registers[$reg.0 as usize + frame.register_offset]
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! reg {
|
||||
($reg:expr) => {{
|
||||
*r_reg!($reg)
|
||||
}};
|
||||
}
|
||||
|
||||
loop {
|
||||
match &chunk.tape[frame.pc] {
|
||||
Instruction::ISLT(a, b) => {
|
||||
frame.cond = reg!(a).op_lt(reg!(b))?.cast_bool_unchecked();
|
||||
},
|
||||
Instruction::ISLE(a, b) => {
|
||||
frame.cond = reg!(a).op_leq(reg!(b))?.cast_bool_unchecked();
|
||||
},
|
||||
Instruction::ISEQ(a, b) => {
|
||||
frame.cond = reg!(a).op_eq(reg!(b)).cast_bool_unchecked();
|
||||
},
|
||||
Instruction::ISNEQ(a, b) => {
|
||||
frame.cond = reg!(a).op_neq(reg!(b)).cast_bool_unchecked();
|
||||
},
|
||||
Instruction::IST(a) => {
|
||||
frame.cond = reg!(a).is_truthy();
|
||||
},
|
||||
Instruction::ISF(a) => {
|
||||
frame.cond = !reg!(a).is_truthy();
|
||||
},
|
||||
Instruction::MOV(a, b) => {
|
||||
*r_reg!(a) = reg!(b);
|
||||
},
|
||||
Instruction::NOT(a, b) => {
|
||||
*r_reg!(a) = reg!(b).op_not();
|
||||
},
|
||||
Instruction::NEG(a, b) => {
|
||||
*r_reg!(a) = reg!(b).op_neg()?;
|
||||
},
|
||||
Instruction::LEN(a, b) => {
|
||||
*r_reg!(a) = reg!(b).op_len()?;
|
||||
},
|
||||
Instruction::BITNOT(a, b) => {
|
||||
*r_reg!(a) = reg!(b).op_bit_not()?;
|
||||
},
|
||||
Instruction::ADD(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_add(reg!(c))?;
|
||||
},
|
||||
Instruction::SUB(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_sub(reg!(c))?;
|
||||
},
|
||||
Instruction::MUL(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_mul(reg!(c))?;
|
||||
},
|
||||
Instruction::DIV(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_div(reg!(c))?;
|
||||
},
|
||||
Instruction::INTDIV(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_int_div(reg!(c))?;
|
||||
},
|
||||
Instruction::EXP(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_exp(reg!(c))?;
|
||||
},
|
||||
Instruction::MOD(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_mod(reg!(c))?;
|
||||
},
|
||||
Instruction::BITAND(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_bit_and(reg!(c))?;
|
||||
},
|
||||
Instruction::BITOR(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_bit_or(reg!(c))?;
|
||||
},
|
||||
Instruction::BITXOR(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_bit_xor(reg!(c))?;
|
||||
},
|
||||
Instruction::LEFTSHIFT(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_lshift(reg!(c))?;
|
||||
},
|
||||
Instruction::RIGHTSHIFT(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_rshift(reg!(c))?;
|
||||
},
|
||||
Instruction::AND(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_and(reg!(c));
|
||||
},
|
||||
Instruction::OR(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_or(reg!(c));
|
||||
},
|
||||
Instruction::CONCAT(a, b, c) => {
|
||||
*r_reg!(a) = reg!(b).op_concat(reg!(c), &mut |buf| {
|
||||
intern(&self.heap, &mut self.strings, buf)
|
||||
})?;
|
||||
},
|
||||
Instruction::CLOAD(dst, idx) => {
|
||||
*r_reg!(dst) = chunk.constants[idx.0 as usize];
|
||||
},
|
||||
Instruction::UGET(dst, key) =>
|
||||
*r_reg!(dst) = unsafe {
|
||||
(*frame.function.cast::<Function>())
|
||||
.get_upvalue(&mut self.registers, key.0)
|
||||
},
|
||||
Instruction::USET(dst, key) => unsafe {
|
||||
let value = reg!(dst);
|
||||
(*frame.function.cast::<Function>()).set_upvalue(
|
||||
&mut self.registers,
|
||||
key.0,
|
||||
value,
|
||||
)
|
||||
},
|
||||
Instruction::FNEW(dst, chunk, captures) => {
|
||||
let upvalues = &mut captures.iter().map(|capture| {
|
||||
self.heap
|
||||
.allocate_upvalue(capture.slot.0 as usize + frame.register_offset)
|
||||
});
|
||||
|
||||
let function = self.heap.allocate_function(
|
||||
chunk.0 as u32,
|
||||
captures.len() as u32,
|
||||
upvalues,
|
||||
);
|
||||
|
||||
*r_reg!(dst) = Value::from_function(function);
|
||||
},
|
||||
Instruction::TNEW(dst) => {
|
||||
let table = self.heap.allocate_table(0);
|
||||
*r_reg!(dst) = Value::from_table2(table);
|
||||
},
|
||||
Instruction::TGET(table, dst, key) => {
|
||||
let table = reg!(table)
|
||||
.cast_mut_table2()
|
||||
.ok_or_else(|| Error::new("attempted to get value from non-table"))?;
|
||||
|
||||
*r_reg!(dst) = table.get(reg!(key));
|
||||
},
|
||||
Instruction::TSET(table, key, value) => {
|
||||
let table = reg!(table)
|
||||
.cast_mut_table2()
|
||||
.ok_or_else(|| Error::new("attempted to set key in non-table"))?;
|
||||
|
||||
table.set(reg!(key), reg!(value));
|
||||
},
|
||||
Instruction::GGET(dst, key) => {
|
||||
let table = unsafe { &*self.global.cast::<Table2>() };
|
||||
*r_reg!(dst) = table.get(reg!(key));
|
||||
},
|
||||
Instruction::GSET(key, value) => {
|
||||
let table = unsafe { &mut *self.global.cast::<Table2>() };
|
||||
table.set(reg!(key), reg!(value));
|
||||
},
|
||||
Instruction::JMP(new_pc) => {
|
||||
frame.pc = new_pc.0 as usize;
|
||||
continue;
|
||||
},
|
||||
Instruction::APUSH(src) => {
|
||||
self.acall.push_front(reg!(src));
|
||||
},
|
||||
Instruction::APOP(dst) => {
|
||||
*r_reg!(dst) = self.acall.pop_back().unwrap();
|
||||
},
|
||||
Instruction::ACLEAR => {
|
||||
self.acall.clear();
|
||||
},
|
||||
Instruction::CALL(src) => {
|
||||
let function = reg!(src)
|
||||
.cast_function()
|
||||
.ok_or_else(|| Error::new("attempted to call non-fuction"))?;
|
||||
|
||||
let frame = precall(
|
||||
&mut self.registers,
|
||||
function,
|
||||
&self.chunks,
|
||||
frame.register_offset,
|
||||
chunk.register_count,
|
||||
);
|
||||
self.callstack.push(frame);
|
||||
continue 'call;
|
||||
},
|
||||
Instruction::RET => {
|
||||
self.callstack.pop();
|
||||
continue 'call;
|
||||
// TODO: close upvalues
|
||||
},
|
||||
}
|
||||
|
||||
frame.pc += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn precall(
|
||||
registers: &mut Vec<Value>,
|
||||
function: ObjectPtr,
|
||||
chunks: &HashMap<u32, Chunk>,
|
||||
register_offset: usize,
|
||||
register_count: usize,
|
||||
) -> CallFrame {
|
||||
let func = unsafe { &*function.cast::<Function>() };
|
||||
let chunk_id = func.chunk();
|
||||
let new_chunk = &chunks[&chunk_id];
|
||||
let register_offset = register_offset + register_count;
|
||||
let req_reg = register_offset + 1 + new_chunk.register_count;
|
||||
|
||||
if req_reg > registers.len() {
|
||||
let addi = registers.len() - req_reg;
|
||||
registers.reserve(addi);
|
||||
|
||||
for _ in 0..addi {
|
||||
registers.push(Value::nil());
|
||||
}
|
||||
}
|
||||
|
||||
CallFrame {
|
||||
function,
|
||||
register_offset,
|
||||
pc: 0,
|
||||
cond: false,
|
||||
}
|
||||
}
|
||||
|
||||
struct CallFrame {
|
||||
function: ObjectPtr,
|
||||
register_offset: usize,
|
||||
pc: usize,
|
||||
cond: bool,
|
||||
}
|
||||
|
||||
struct Chunk {
|
||||
tape: Vec<Instruction>,
|
||||
constants: Vec<Value>,
|
||||
register_count: usize,
|
||||
arity: usize,
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
use std::{
|
||||
borrow::Borrow,
|
||||
cell::{Ref, RefCell},
|
||||
collections::hash_map::RandomState,
|
||||
};
|
||||
|
||||
use cstree::interning::TokenInterner;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
|
||||
use super::super::{
|
||||
gc::{Handle, Heap},
|
||||
value::{ByteString, Table, Value},
|
||||
};
|
||||
use crate::parser::syntax::Ident;
|
||||
|
||||
struct CtxInternal<'a> {
|
||||
global: &'a mut Table,
|
||||
scope: Vec<HashMap<Handle<ByteString>, Value, RandomState>>,
|
||||
heap: &'a Heap,
|
||||
interner: &'a TokenInterner,
|
||||
strings: &'a mut HashSet<Handle<ByteString>, RandomState>,
|
||||
}
|
||||
|
||||
pub struct Ctx<'a> {
|
||||
internal: RefCell<CtxInternal<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Ctx<'a> {
|
||||
pub fn new(
|
||||
global: &'a mut Table,
|
||||
heap: &'a Heap,
|
||||
interner: &'a TokenInterner,
|
||||
strings: &'a mut HashSet<Handle<ByteString>, RandomState>,
|
||||
) -> Self {
|
||||
Ctx {
|
||||
internal: RefCell::new(CtxInternal {
|
||||
global,
|
||||
scope: vec![HashMap::with_hasher(RandomState::new())],
|
||||
heap,
|
||||
interner,
|
||||
strings,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn heap(&self) -> Ref<Heap> {
|
||||
Ref::map(self.internal.borrow(), |internal| internal.heap)
|
||||
}
|
||||
|
||||
pub fn scope(&self) -> ScopeKey<'a, '_> {
|
||||
let mut internal = self.internal.borrow_mut();
|
||||
internal
|
||||
.scope
|
||||
.push(HashMap::with_hasher(RandomState::new()));
|
||||
|
||||
ScopeKey { ctx: self }
|
||||
}
|
||||
|
||||
fn scope_destroy(&self) {
|
||||
let mut internal = self.internal.borrow_mut();
|
||||
internal.scope.pop();
|
||||
}
|
||||
|
||||
pub fn local(&self, key: Handle<ByteString>) {
|
||||
self.internal
|
||||
.borrow_mut()
|
||||
.scope
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.insert(key, Value::from_nil());
|
||||
}
|
||||
|
||||
pub fn assign(&self, key: Handle<ByteString>, value: Value) {
|
||||
let mut internal = self.internal.borrow_mut();
|
||||
|
||||
for scope in internal.scope.iter_mut().rev() {
|
||||
if scope.contains_key(&key) {
|
||||
scope.insert(key, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let key = Value::from_string(key);
|
||||
internal.global.insert(key, value);
|
||||
}
|
||||
|
||||
pub fn resolve(&self, key: Handle<ByteString>) -> Value {
|
||||
let internal = self.internal.borrow();
|
||||
|
||||
for scope in internal.scope.iter().rev() {
|
||||
if let Some(value) = scope.get(&key) {
|
||||
return *value;
|
||||
}
|
||||
}
|
||||
|
||||
let key = Value::from_string(key);
|
||||
internal.global.get(key)
|
||||
}
|
||||
|
||||
pub fn intern(&self, key: &[u8]) -> Handle<ByteString> {
|
||||
let mut internal = self.internal.borrow_mut();
|
||||
|
||||
impl Borrow<[u8]> for Handle<ByteString> {
|
||||
fn borrow(&self) -> &[u8] {
|
||||
unsafe { self.get_unchecked() }
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(handle) = internal.strings.get(key) {
|
||||
return *handle;
|
||||
}
|
||||
|
||||
let handle = internal.heap.insert_string(key);
|
||||
internal.strings.insert(handle);
|
||||
handle
|
||||
}
|
||||
|
||||
pub fn intern_ident(&self, ident: &Ident) -> Handle<ByteString> {
|
||||
let internal = self.internal.borrow();
|
||||
let name = ident.name(internal.interner).unwrap();
|
||||
drop(internal);
|
||||
self.intern(name.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScopeKey<'a, 'ctx> {
|
||||
ctx: &'ctx Ctx<'a>,
|
||||
}
|
||||
|
||||
impl<'a, 'ctx> Drop for ScopeKey<'a, 'ctx> {
|
||||
fn drop(&mut self) {
|
||||
self.ctx.scope_destroy();
|
||||
}
|
||||
}
|
|
@ -1,394 +0,0 @@
|
|||
use std::{convert::Infallible, ops};
|
||||
|
||||
use super::{
|
||||
super::{value::Value, Error},
|
||||
ctx::Ctx,
|
||||
};
|
||||
use crate::parser::syntax::{
|
||||
Assign,
|
||||
BinaryOp,
|
||||
BinaryOperator,
|
||||
Break,
|
||||
Decl,
|
||||
Do,
|
||||
Expr,
|
||||
ForGen,
|
||||
ForNum,
|
||||
Func,
|
||||
FuncCall,
|
||||
FuncExpr,
|
||||
Ident,
|
||||
If,
|
||||
Index,
|
||||
Literal,
|
||||
PrefixOp,
|
||||
PrefixOperator,
|
||||
Repeat,
|
||||
Return,
|
||||
Root,
|
||||
Stmt,
|
||||
Table,
|
||||
While,
|
||||
};
|
||||
|
||||
pub trait Eval {
|
||||
fn eval(&self, ctx: &Ctx) -> Result;
|
||||
}
|
||||
|
||||
pub enum Result<T = Value> {
|
||||
Value(T),
|
||||
Return(Vec<Value>),
|
||||
Break,
|
||||
Error(Error),
|
||||
}
|
||||
|
||||
impl ops::Try for Result {
|
||||
type Output = Value;
|
||||
type Residual = Result<Infallible>;
|
||||
|
||||
fn from_output(value: Value) -> Self {
|
||||
Result::Value(value)
|
||||
}
|
||||
|
||||
fn branch(self) -> ops::ControlFlow<Self::Residual, Self::Output> {
|
||||
match self {
|
||||
Result::Value(value) => ops::ControlFlow::Continue(value),
|
||||
Result::Return(value) => ops::ControlFlow::Break(Result::Return(value)),
|
||||
Result::Break => ops::ControlFlow::Break(Result::Break),
|
||||
Result::Error(error) => ops::ControlFlow::Break(Result::Error(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::FromResidual for Result {
|
||||
fn from_residual(residual: Result<Infallible>) -> Result {
|
||||
match residual {
|
||||
Result::Value(_) => panic!(),
|
||||
Result::Return(value) => Result::Return(value),
|
||||
Result::Break => Result::Break,
|
||||
Result::Error(error) => Result::Error(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result> for std::result::Result<Value, Error> {
|
||||
fn from(result: Result) -> std::result::Result<Value, Error> {
|
||||
match result {
|
||||
Result::Value(value) => Ok(value),
|
||||
Result::Return(_) => Err(Error::UncaughtReturn),
|
||||
Result::Break => Err(Error::UncaughtBreak),
|
||||
Result::Error(error) => Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Root {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
for stmt in self.block() {
|
||||
stmt.eval(ctx)?;
|
||||
}
|
||||
|
||||
Result::Value(Value::from_nil())
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Stmt {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
match self {
|
||||
Self::Decl(decl) => decl.eval(ctx),
|
||||
Self::Assign(assign) => assign.eval(ctx),
|
||||
Self::Func(func) => func.eval(ctx),
|
||||
Self::Expr(expr) => expr.eval(ctx),
|
||||
Self::Break(r#break) => r#break.eval(ctx),
|
||||
Self::Return(r#return) => r#return.eval(ctx),
|
||||
Self::Do(r#do) => r#do.eval(ctx),
|
||||
Self::While(r#while) => r#while.eval(ctx),
|
||||
Self::Repeat(repeat) => repeat.eval(ctx),
|
||||
Self::If(r#if) => r#if.eval(ctx),
|
||||
Self::ForNum(for_num) => for_num.eval(ctx),
|
||||
Self::ForGen(for_gen) => for_gen.eval(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Decl {
|
||||
fn eval(&self, _ctx: &Ctx) -> Result {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Assign {
|
||||
fn eval(&self, _ctx: &Ctx) -> Result {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Func {
|
||||
fn eval(&self, _ctx: &Ctx) -> Result {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Expr {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
match self {
|
||||
Self::Ident(ident) => ident.eval(ctx),
|
||||
Self::Literal(literal) => literal.eval(ctx),
|
||||
Self::Func(func) => func.eval(ctx),
|
||||
Self::Table(call) => call.eval(ctx),
|
||||
Self::PrefixOp(prefix_op) => prefix_op.eval(ctx),
|
||||
Self::BinaryOp(binary_op) => binary_op.eval(ctx),
|
||||
Self::FuncCall(call) => call.eval(ctx),
|
||||
Self::Index(index) => index.eval(ctx),
|
||||
Self::VarArg => panic!("varargs are currently unsupported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Ident {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
let key = ctx.intern_ident(self);
|
||||
Result::Value(ctx.resolve(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Literal {
|
||||
fn eval(&self, _ctx: &Ctx) -> Result {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for FuncExpr {
|
||||
fn eval(&self, _ctx: &Ctx) -> Result {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Table {
|
||||
fn eval(&self, _ctx: &Ctx) -> Result {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for PrefixOp {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
let rhs = self.rhs().unwrap().eval(ctx)?;
|
||||
|
||||
Result::Value(match self.op().unwrap() {
|
||||
PrefixOperator::None => rhs,
|
||||
PrefixOperator::Neg => rhs.op_neg(),
|
||||
PrefixOperator::Not => rhs.op_not(),
|
||||
PrefixOperator::Len => rhs.op_len(),
|
||||
PrefixOperator::BitNot => rhs.op_bit_not(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for BinaryOp {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
let lhs = self.lhs().unwrap().eval(ctx)?;
|
||||
let rhs = self.rhs().unwrap().eval(ctx)?;
|
||||
|
||||
Result::Value(match self.op().unwrap() {
|
||||
BinaryOperator::And => lhs.op_and(rhs),
|
||||
BinaryOperator::Or => lhs.op_or(rhs),
|
||||
BinaryOperator::Add => lhs.op_add(rhs),
|
||||
BinaryOperator::Sub => lhs.op_sub(rhs),
|
||||
BinaryOperator::Mul => lhs.op_mul(rhs),
|
||||
BinaryOperator::Div => lhs.op_div(rhs),
|
||||
BinaryOperator::IntDiv => lhs.op_int_div(rhs),
|
||||
BinaryOperator::Exp => lhs.op_exp(rhs),
|
||||
BinaryOperator::Mod => lhs.op_mod(rhs),
|
||||
BinaryOperator::BitAnd => lhs.op_bit_and(rhs),
|
||||
BinaryOperator::BitOr => lhs.op_bit_or(rhs),
|
||||
BinaryOperator::LShift => lhs.op_lshift(rhs),
|
||||
BinaryOperator::RShift => lhs.op_rshift(rhs),
|
||||
BinaryOperator::Eq => lhs.op_eq(rhs),
|
||||
BinaryOperator::BitXor => lhs.op_bit_xor(rhs),
|
||||
BinaryOperator::NEq => lhs.op_neq(rhs),
|
||||
BinaryOperator::LEq => lhs.op_leq(rhs),
|
||||
BinaryOperator::GEq => lhs.op_geq(rhs),
|
||||
BinaryOperator::Gt => lhs.op_gt(rhs),
|
||||
BinaryOperator::Lt => lhs.op_lt(rhs),
|
||||
BinaryOperator::Property => lhs.op_property(rhs),
|
||||
BinaryOperator::Method => lhs.op_method(rhs),
|
||||
BinaryOperator::Concat => lhs.op_concat(rhs, ctx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Index {
|
||||
fn eval(&self, _ctx: &Ctx) -> Result {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for FuncCall {
|
||||
fn eval(&self, _ctx: &Ctx) -> Result {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Break {
|
||||
fn eval(&self, _ctx: &Ctx) -> Result {
|
||||
Result::Break
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Return {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
let mut values = Vec::new();
|
||||
for expr in self.exprs().unwrap() {
|
||||
values.push(expr.eval(ctx)?);
|
||||
}
|
||||
|
||||
Result::Return(values)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Do {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
let _scope = ctx.scope();
|
||||
for stmt in self.stmts() {
|
||||
stmt.eval(ctx)?;
|
||||
}
|
||||
|
||||
Result::Value(Value::from_nil())
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for While {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
while self.cond().unwrap().eval(ctx)?.cast_bool_unchecked() {
|
||||
let _scope = ctx.scope();
|
||||
for stmt in self.block().unwrap() {
|
||||
let res = stmt.eval(ctx);
|
||||
if let Result::Break = res {
|
||||
break;
|
||||
}
|
||||
|
||||
res?;
|
||||
}
|
||||
}
|
||||
|
||||
Result::Value(Value::from_nil())
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Repeat {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
loop {
|
||||
let _scope = ctx.scope();
|
||||
for stmt in self.block().unwrap() {
|
||||
let res = stmt.eval(ctx);
|
||||
if let Result::Break = res {
|
||||
break;
|
||||
}
|
||||
|
||||
res?;
|
||||
}
|
||||
|
||||
if self.cond().unwrap().eval(ctx)?.cast_bool_unchecked() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Result::Value(Value::from_nil())
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for If {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
if self.cond().unwrap().eval(ctx)?.cast_bool_unchecked() {
|
||||
let _scope = ctx.scope();
|
||||
for stmt in self.stmts().unwrap() {
|
||||
let res = stmt.eval(ctx);
|
||||
if let Result::Break = res {
|
||||
break;
|
||||
}
|
||||
|
||||
res?;
|
||||
}
|
||||
} else if let Some(elif) = self.else_chain() {
|
||||
if let Some(el_if) = elif.elseif_block() {
|
||||
el_if.eval(ctx)?;
|
||||
} else if let Some(el) = elif.else_block() {
|
||||
let _scope = ctx.scope();
|
||||
for stmt in el {
|
||||
let res = stmt.eval(ctx);
|
||||
if let Result::Break = res {
|
||||
break;
|
||||
}
|
||||
|
||||
res?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result::Value(Value::from_nil())
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for ForNum {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
let (counter, init) = self.counter().unwrap();
|
||||
let init = init.eval(ctx)?;
|
||||
let end = self.end().unwrap().eval(ctx)?;
|
||||
let step = if let Some(expr) = self.step() {
|
||||
expr.eval(ctx)?
|
||||
} else {
|
||||
Value::from_int(1)
|
||||
};
|
||||
|
||||
let _scope = ctx.scope();
|
||||
let var = ctx.intern_ident(&counter);
|
||||
ctx.local(var);
|
||||
ctx.assign(var, init);
|
||||
|
||||
while ctx.resolve(var).op_eq(end).cast_bool_unchecked() {
|
||||
let _scope = ctx.scope();
|
||||
for stmt in self.block().unwrap() {
|
||||
stmt.eval(ctx)?;
|
||||
}
|
||||
|
||||
let value = ctx.resolve(var);
|
||||
let value = value.op_add(step);
|
||||
ctx.assign(var, value);
|
||||
}
|
||||
|
||||
Result::Value(Value::from_nil())
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for ForGen {
|
||||
fn eval(&self, ctx: &Ctx) -> Result {
|
||||
let _scope = ctx.scope();
|
||||
|
||||
for target in self.targets().unwrap() {
|
||||
let var = ctx.intern_ident(&target);
|
||||
ctx.local(var);
|
||||
}
|
||||
|
||||
loop {
|
||||
for (target, value) in self.targets().unwrap().zip(self.values().unwrap()) {
|
||||
let var = ctx.intern_ident(&target);
|
||||
let value = value.eval(ctx)?;
|
||||
ctx.assign(var, value);
|
||||
}
|
||||
|
||||
let first_target = self.targets().unwrap().next().unwrap();
|
||||
let first_var = ctx.intern_ident(&first_target);
|
||||
|
||||
if ctx.resolve(first_var) == Value::from_nil() {
|
||||
break;
|
||||
}
|
||||
|
||||
let _scope = ctx.scope();
|
||||
for stmt in self.block().unwrap() {
|
||||
stmt.eval(ctx)?;
|
||||
}
|
||||
}
|
||||
|
||||
Result::Value(Value::from_nil())
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
pub mod ctx;
|
||||
pub mod eval;
|
||||
|
||||
use std::collections::hash_map::RandomState;
|
||||
|
||||
use cstree::interning::TokenInterner;
|
||||
use ctx::Ctx;
|
||||
use eval::Eval;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
|
||||
use super::{
|
||||
gc::{Handle, Heap},
|
||||
value::{ByteString, Table, Value},
|
||||
Error,
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// - vm eval impl
|
||||
// - gc root tracked values in the api
|
||||
// - impl _ENV
|
||||
// - handle multivalue
|
||||
pub struct VM {
|
||||
global: Table,
|
||||
strings: HashSet<Handle<ByteString>, RandomState>,
|
||||
extern_ref: HashMap<Value, usize, RandomState>,
|
||||
}
|
||||
|
||||
impl VM {
|
||||
pub fn new(heap: Heap) -> Self {
|
||||
VM {
|
||||
global: Table::new(heap),
|
||||
strings: HashSet::with_hasher(RandomState::new()),
|
||||
extern_ref: HashMap::with_hasher(RandomState::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval<T>(
|
||||
&mut self,
|
||||
item: &T,
|
||||
heap: &Heap,
|
||||
interner: &TokenInterner,
|
||||
) -> Result<Value, Error>
|
||||
where
|
||||
T: Eval,
|
||||
{
|
||||
let ctx = Ctx::new(&mut self.global, heap, interner, &mut self.strings);
|
||||
item.eval(&ctx).into()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
#![feature(allocator_api)]
|
||||
#![feature(cell_update)]
|
||||
#![feature(try_trait_v2)]
|
||||
#![feature(hash_raw_entry)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod engine;
|
||||
|
|
Loading…
Reference in a new issue