Added parser, compiler, virtual machine, garbage collector, tables and values. Builds but may have errors.
This commit is contained in:
commit
5d4fac0a91
8 changed files with 3199 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.idea
|
||||
target
|
||||
16
Cargo.lock
generated
Normal file
16
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.33.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360bd2cd76e0cd9032d42cf2922155cecea2685b0cfa4630c3246df030bcfd6"
|
||||
|
||||
[[package]]
|
||||
name = "mars"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"glam",
|
||||
]
|
||||
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "mars"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
vector3 = ["dep:glam"]
|
||||
messages = []
|
||||
assertions = []
|
||||
|
||||
[dependencies]
|
||||
glam = { version = "0.33.3", optional = true }
|
||||
0
README.md
Normal file
0
README.md
Normal file
139
src/gc.rs
Normal file
139
src/gc.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use std::alloc::{alloc, dealloc, Layout};
|
||||
use std::any::Any;
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::fmt::Debug;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::ptr::NonNull;
|
||||
// A simple 'stop the world' 'mark and sweep' garbage collector
|
||||
// This approach is used since game engine frames provide a nice 'resting' interval for the interpreter to clean up faster
|
||||
// todo! generational and naive reference counted collection
|
||||
/* naive reference counting could be done as:
|
||||
keep a traditional reference count =>
|
||||
- field count: u8 (>256 references -> a cycle will prevent premature deallocation anyway)
|
||||
if the count reaches zero, prematurely deallocate by using an optional soft 'drop' method in the VM =>
|
||||
- field location: usize (location of this pointer in the big 'objects' array of the allocator)
|
||||
- drop(&self,allocator) {
|
||||
if count == 0 {
|
||||
allocator[self.pointer->location] = nullptr; // make allocator forget this
|
||||
dealloc(self.pointer); // deallocate now
|
||||
}
|
||||
}
|
||||
in the allocator ensure location is consistent
|
||||
|
||||
*/
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Gc<T: ?Sized + Traverse> {
|
||||
it: NonNull<RefCell<Header<T>>>
|
||||
}
|
||||
|
||||
pub struct Agc {}
|
||||
|
||||
impl<T: ?Sized + Traverse> Clone for Gc<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Gc { it: self.it }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + Traverse> Traverse for Gc<T> {
|
||||
fn traverse(&self) {
|
||||
self.borrow().traverse()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + Traverse> PartialEq for Gc<T> {
|
||||
fn eq(&self, other: &Gc<T>) -> bool {
|
||||
self.it == other.it
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + Traverse> Gc<T> {
|
||||
pub fn borrow(&self) -> impl Deref<Target=T> {
|
||||
Ref::map(unsafe { // ???
|
||||
(*self.it.as_ptr()).borrow()
|
||||
},|it| &it.data)
|
||||
}
|
||||
pub fn borrow_mut(&mut self) -> impl DerefMut<Target=T> {
|
||||
RefMut::map(unsafe {
|
||||
(*self.it.as_mut()).borrow_mut()
|
||||
},|it| &mut it.data)
|
||||
}
|
||||
|
||||
pub fn replace(&mut self, new: T) where T: Sized {
|
||||
unsafe {
|
||||
(*self.it.as_mut()).borrow_mut().data = new;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addr(&self) -> usize {
|
||||
self.it.as_ptr().addr()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Copy)]
|
||||
enum Color {
|
||||
White,
|
||||
Black,
|
||||
}
|
||||
|
||||
struct Header<T: ?Sized + Traverse> {
|
||||
layout: Layout,
|
||||
color: Color, // NOT for incremental tri-color right now; white == mark-free, black == mark-keep
|
||||
data: T
|
||||
}
|
||||
|
||||
pub struct Allocator {
|
||||
// An array of pointers is used here since I did not want to use a compactor and larger types
|
||||
// held on the GC (game objects) will most likely be massive anyway.
|
||||
objects: Vec<Option<*mut RefCell<Header<dyn Traverse>>>>,
|
||||
}
|
||||
|
||||
impl Allocator {
|
||||
pub fn new() -> Allocator {
|
||||
Allocator {
|
||||
objects: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub fn alloc<T: Traverse + 'static>(&mut self, it: T) -> Gc<T> {
|
||||
let layout = Layout::new::<RefCell<Header<T>>>();
|
||||
let pointer = unsafe { alloc(layout) as *mut RefCell<Header<T>> }; // todo: maybe use a compacting GC
|
||||
assert!(!pointer.is_null()); // lol idk how to fix this rn
|
||||
let mut header = unsafe { pointer.as_mut() }.unwrap().borrow_mut();
|
||||
header.layout = layout;
|
||||
header.color = Color::Black;
|
||||
header.data = it;
|
||||
self.objects.push(Some(pointer as *mut RefCell<Header<dyn Traverse>>));
|
||||
Gc {
|
||||
it: NonNull::new(pointer).unwrap(),
|
||||
}
|
||||
}
|
||||
fn mark_white(&mut self) {
|
||||
for i in 0..self.objects.len() {
|
||||
if let Some(object) = self.objects[i] {
|
||||
unsafe { (*object).borrow_mut().color = Color::White };
|
||||
}
|
||||
}
|
||||
}
|
||||
fn collect(&mut self) {
|
||||
self.objects.retain(|object| {
|
||||
if let Some(object) = object {
|
||||
let header = unsafe { (**object).borrow_mut() };
|
||||
match header.color {
|
||||
Color::White => {
|
||||
unsafe { dealloc(*object as *mut u8,header.layout); }
|
||||
true
|
||||
}
|
||||
Color::Black => false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Traverse: Any + Debug {
|
||||
fn traverse(&self) { // mark or grey
|
||||
()
|
||||
}
|
||||
}
|
||||
2675
src/lib.rs
Normal file
2675
src/lib.rs
Normal file
File diff suppressed because it is too large
Load diff
340
src/table.rs
Normal file
340
src/table.rs
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::rc::Rc;
|
||||
use crate::gc::{Gc, Traverse};
|
||||
use crate::{Callable, RunError, Value};
|
||||
|
||||
// I've already spent 2 months on this interpreter, and I'm tired, so I've cut a few corners...
|
||||
/* See some of Lua's notes on this table: https://www.lua.org/source/5.5/ltable.c.html
|
||||
This table uses an 'open-addressed' hashmap https://en.wikipedia.org/wiki/Open_addressing
|
||||
where collisions are handled by 'linear-probing' and something I call 'displacement'.
|
||||
|
||||
Motivation:
|
||||
Afaik, without having some mechanism that records or ensures known proximity between a displaced element
|
||||
that collided with other(s) in the hashmap and it's original home (at element's hash's index),
|
||||
the entire array may have to be checked just to see if an element is present. How can we fix this?
|
||||
|
||||
Solution:
|
||||
Lua uses Brent's method, which I couldn't find a concrete explanation of, here I'm just recording
|
||||
the maximum displacement that will need to be linearly-probed from the home entry to be certain of
|
||||
any 'home' element's presence in the greater array.
|
||||
|
||||
Notes:
|
||||
- If this value becomes large, the load factor is probably high and there would be a resize.
|
||||
- Values would only need to be shuffled around closer to their 'home' during a resize.
|
||||
- It is unlikely the displacement would large value without a resize amending that problem.
|
||||
*/
|
||||
|
||||
type Displacement = u8;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Entry {
|
||||
index: Value,
|
||||
item: Value,
|
||||
home: usize,
|
||||
displacement: Displacement,
|
||||
}
|
||||
|
||||
impl Default for Entry {
|
||||
fn default() -> Self {
|
||||
Entry {
|
||||
index: Value::Nil,
|
||||
item: Value::Nil,
|
||||
home: 0,
|
||||
displacement: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash(value: &Value) -> usize {
|
||||
match value {
|
||||
Value::Nil => { 0 }
|
||||
Value::Bool(boolean) => { if *boolean { 1 } else { 0 } }
|
||||
Value::String(string) => {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
string.hash(&mut hasher);
|
||||
hasher.finish() as usize
|
||||
}
|
||||
Value::Phrase(phrase) => {
|
||||
phrase.1
|
||||
}
|
||||
Value::Function(callable) => {
|
||||
match callable {
|
||||
Callable::Rust(native) => {
|
||||
Rc::as_ptr(native).addr()
|
||||
}
|
||||
Callable::Mars(closure) => {
|
||||
closure.addr()
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Integer(integer) => {
|
||||
*integer as usize
|
||||
}
|
||||
Value::Number(number) => {
|
||||
if number.is_nan() {
|
||||
0
|
||||
} else {
|
||||
number.to_bits() as usize
|
||||
}
|
||||
}
|
||||
Value::Table(table) => {
|
||||
table.addr()
|
||||
}
|
||||
Value::Object(object) => {
|
||||
object.addr()
|
||||
}
|
||||
#[cfg(feature = "vector3")]
|
||||
Value::Vector(vec) => {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
vec.as_u64vec3().hash(&mut hasher);
|
||||
hasher.finish() as usize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: displaced items don't ever get shuffled closer to their homes unless the table is resized
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Table {
|
||||
table: Vec<Entry>,
|
||||
array: Vec<Value>,
|
||||
table_bounds: std::ops::Range<usize>,
|
||||
table_count: usize, // number of elements in table
|
||||
pub meta: Option<Gc<Table>>
|
||||
}
|
||||
|
||||
impl Traverse for Table {
|
||||
fn traverse(&self) {
|
||||
for Entry { index, item, .. } in self.table.iter() {
|
||||
if !matches!(index,Value::Nil) {
|
||||
index.traverse();
|
||||
item.traverse();
|
||||
}
|
||||
}
|
||||
for item in self.array.iter() {
|
||||
item.traverse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Table {
|
||||
fn exchange_table(&mut self, len: usize) {
|
||||
let old = std::mem::replace(&mut self.table, vec![Entry::default(); len]);
|
||||
for Entry { index, item, .. } in old {
|
||||
if index != Value::Nil {
|
||||
self.set_table(index,item)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn resize_table(&mut self, len: usize) {
|
||||
self.exchange_table(len.max(self.table_count + (self.table_count / 3)));
|
||||
self.table_bounds = 0..self.table_upper();
|
||||
}
|
||||
pub fn resize_array(&mut self, len: usize) {
|
||||
self.array.resize(len,Value::Nil)
|
||||
}
|
||||
fn table_upper(&self) -> usize {
|
||||
(self.table.len() / 4) * 3
|
||||
}
|
||||
fn table_lower(&self) -> usize {
|
||||
self.table.len() / 3
|
||||
}
|
||||
fn ensure_table(&mut self) {
|
||||
if self.table_count > self.table_upper() { // free space is short
|
||||
self.exchange_table((self.table.len() + 4) * 2);
|
||||
} else if self.table_count < self.table_lower() { // too much free space
|
||||
self.exchange_table(self.table.len() / 2)
|
||||
}
|
||||
self.table_bounds = self.table_lower()..self.table_upper()
|
||||
}
|
||||
fn set_table(&mut self, index: Value, item: Value) {
|
||||
#[cfg(feature = "assertions")]
|
||||
assert_ne!(index,Value::Nil);
|
||||
#[cfg(feature = "assertions")]
|
||||
assert_ne!(item,Value::Nil);
|
||||
if !self.table_bounds.contains(&self.table_count) { // make sure the table is appropriately sized
|
||||
self.ensure_table();
|
||||
}
|
||||
let range = self.table.len();
|
||||
let home = hash(&index) % range;
|
||||
if self.table[home].index == Value::Nil { // attempt to place it directly in an empty space
|
||||
// INSERT
|
||||
self.table[home].home = home;
|
||||
self.table[home].item = item; // home.item.soft_drop()
|
||||
self.table[home].index = index;
|
||||
self.table[home].displacement = self.table[home].displacement.max(0);
|
||||
self.table_count += 1;
|
||||
} else if self.table[home].index == index { // attempt to replace it directly
|
||||
// REPLACE
|
||||
self.table[home].item = item; // home.item.soft_drop()
|
||||
} else {
|
||||
// attempt to replace it in a collided neighbour location
|
||||
for i in 1..self.table[home].displacement + 1 {
|
||||
let neighbour = &mut self.table[(home + i as usize) % range]; // todo: is mod expensive?
|
||||
if neighbour.index == index { // found where it was displaced to
|
||||
// REPLACE
|
||||
neighbour.item = item;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// at this point it must be added, probe to place in an empty space
|
||||
for j in self.table[home].displacement + 1..Displacement::MAX - 1 {
|
||||
let neighbour = &mut self.table[(home + j as usize) % range];
|
||||
if neighbour.index == Value::Nil { // new empty slot hooray!
|
||||
// INSERT
|
||||
neighbour.home = home;
|
||||
neighbour.item = item;
|
||||
neighbour.index = index;
|
||||
self.table[home].displacement = j;
|
||||
self.table_count += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// impossible but this still needs to be complete
|
||||
#[cfg(feature = "messages")]
|
||||
eprintln!("Large table collision, are the hashes ok?\n\tBrute-force probing...");
|
||||
let mut free: Option<usize> = None;
|
||||
for k in 0..self.table.len() {
|
||||
let neighbour = &mut self.table[k];
|
||||
if neighbour.index == Value::Nil && free.is_none() {
|
||||
free = Some(k);
|
||||
} else if neighbour.index == index {
|
||||
// REPLACE
|
||||
neighbour.item = item;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(k) = free {
|
||||
// INSERT
|
||||
self.table[k].home = home;
|
||||
self.table[k].index = index;
|
||||
self.table[k].item = item;
|
||||
self.table[home].displacement = Displacement::MAX;
|
||||
self.table_count += 1;
|
||||
return;
|
||||
}
|
||||
// must resize
|
||||
#[cfg(feature = "messages")]
|
||||
eprintln!("\n\tResizing...");
|
||||
self.resize_table((self.table.len() + 4) * 2);
|
||||
self.set_table(index,item);
|
||||
}
|
||||
}
|
||||
fn rem_table(&mut self, index: Value) {
|
||||
let range = self.table.len();
|
||||
let home = hash(&index) % range;
|
||||
if self.table[home].index == index {
|
||||
self.table[home].index = Value::Nil;
|
||||
self.table[home].item = Value::Nil; // is this necessary?
|
||||
self.table_count -= 1;
|
||||
} else {
|
||||
if self.table[home].displacement != Displacement::MAX {
|
||||
let mut largest = 0;
|
||||
for i in 1..self.table[home].displacement {
|
||||
let neighbour = &mut self.table[(home + i as usize) % range];
|
||||
if neighbour.index == index {
|
||||
neighbour.index = Value::Nil;
|
||||
neighbour.item = Value::Nil;
|
||||
self.table_count -= 1;
|
||||
if i == self.table[home].displacement {
|
||||
self.table[home].displacement = largest
|
||||
}
|
||||
return;
|
||||
} else if neighbour.index != Value::Nil && neighbour.home == home {
|
||||
largest = i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut largest = 0;
|
||||
let mut finished = false; // what
|
||||
for k in 0..self.table.len() {
|
||||
let neighbour = &mut self.table[k];
|
||||
if neighbour.index == index {
|
||||
neighbour.index = Value::Nil;
|
||||
neighbour.item = Value::Nil;
|
||||
self.table_count -= 1;
|
||||
#[cfg(feature = "assertions")]
|
||||
assert!(!finished);
|
||||
finished = true;
|
||||
}
|
||||
if neighbour.home == home && neighbour.index != Value::Nil {
|
||||
largest = largest.max(if k < home {
|
||||
k + self.table.len() - home - 1 // ?
|
||||
} else {
|
||||
k - home
|
||||
})
|
||||
}
|
||||
}
|
||||
self.table[home].displacement = if largest > Displacement::MAX as usize {
|
||||
Displacement::MAX
|
||||
} else {
|
||||
largest as Displacement
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn set(&mut self, index: Value, item: Value) -> Result<(),RunError> {
|
||||
match index {
|
||||
Value::Integer(index) => { // This is an integer index, try the array first
|
||||
match item {
|
||||
Value::Nil => {
|
||||
if (0..self.array.len() + 1).contains(&(index as usize)) {
|
||||
if self.array.len() == index as usize {
|
||||
self.array.pop();
|
||||
} else {
|
||||
self.array[index as usize + 1] = Value::Nil
|
||||
}
|
||||
}
|
||||
self.rem_table(Value::Integer(index));
|
||||
},
|
||||
item => {
|
||||
if (0..self.array.len() + 1).contains(&(index as usize)) {
|
||||
if self.array.len() == index as usize {
|
||||
self.array.push(item)
|
||||
} else {
|
||||
self.array[index as usize + 1] = item;
|
||||
}
|
||||
} else {
|
||||
self.set_table(Value::Integer(index),item)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
Value::Nil => Err(RunError("Attempt to set new index of tabel with key: nil".to_string())),
|
||||
index => Ok(self.set_table(index,item))
|
||||
}
|
||||
}
|
||||
pub fn get(&self, index: Value) -> Result<Value,RunError> {
|
||||
match index {
|
||||
Value::Integer(index) => Ok(self.array.get(index as usize).unwrap_or(&Value::Nil).clone()),
|
||||
Value::Nil => Err(RunError("Attempt to index table with key: nil".to_string())),
|
||||
index => {
|
||||
let location = hash(&index) % self.table.len();
|
||||
let home = &self.table[location];
|
||||
if home.index == index {
|
||||
Ok(home.item.clone())
|
||||
} else {
|
||||
for i in 1..home.displacement as usize {
|
||||
let neighbour = &self.table[location + i];
|
||||
if neighbour.index == index {
|
||||
return Ok(neighbour.item.clone())
|
||||
}
|
||||
}
|
||||
Ok(Value::Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn append(&mut self, item: Value) {
|
||||
self.array.push(item)
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Table {
|
||||
table: Vec::new(),
|
||||
array: Vec::new(),
|
||||
table_count: 0,
|
||||
table_bounds: (0..0).into(),
|
||||
meta: None
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/tests/script.lua
Normal file
15
src/tests/script.lua
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
local two
|
||||
do
|
||||
local one
|
||||
if something then
|
||||
local three
|
||||
print(something)
|
||||
end
|
||||
local four
|
||||
if something2 then
|
||||
|
||||
end
|
||||
end
|
||||
local function bricked(hello, there)
|
||||
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue