Added parser, compiler, virtual machine, garbage collector, tables and values. Builds but may have errors.

This commit is contained in:
paladin 2026-08-19 23:28:35 +01:00
commit 5d4fac0a91
8 changed files with 3199 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.idea
target

16
Cargo.lock generated Normal file
View 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
View 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
View file

139
src/gc.rs Normal file
View 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

File diff suppressed because it is too large Load diff

340
src/table.rs Normal file
View 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
View 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