Unified the C# mod and the Rust compiler into a monorepo

This commit is contained in:
2025-11-26 16:02:00 -07:00
parent 346b6e49e6
commit 353dc16944
34 changed files with 253 additions and 14 deletions

View File

@@ -0,0 +1,6 @@
#[cfg(test)]
mod test;
mod v1;
mod variable_manager;
pub use v1::{Compiler, CompilerConfig, Error};

View File

@@ -0,0 +1,100 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn simple_binary_expression() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let i = 1 + 2;
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
add r1 1 2
move r8 r1 #i
"
}
);
Ok(())
}
#[test]
fn nested_binary_expressions() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
fn calculateArgs(arg1, arg2, arg3) {
return (arg1 + arg2) * arg3;
};
let returned = calculateArgs(10, 20, 30) + 100;
"
};
assert_eq!(
compiled,
indoc! {
"
j main
calculateArgs:
pop r8 #arg3
pop r9 #arg2
pop r10 #arg1
push ra
add r1 r10 r9
mul r2 r1 r8
move r15 r2
sub r0 sp 1
get ra db r0
sub sp sp 1
j ra
main:
push 10
push 20
push 30
jal calculateArgs
move r1 r15 #__binary_temp_3
add r2 r1 100
move r8 r2 #returned
"
}
);
Ok(())
}
#[test]
fn stress_test_negation_with_stack_spillover() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let negationHell = (-1 + -2) * (-3 + (-4 * (-5 + -6)));
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
add r1 -1 -2
add r2 -5 -6
mul r3 -4 r2
add r4 -3 r3
mul r5 r1 r4
move r8 r5 #negationHell
"
}
);
Ok(())
}

View File

@@ -0,0 +1,158 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_if_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let a = 10;
if (a > 5) {
a = 20;
}
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 10 #a
sgt r1 r8 5
beq r1 0 L1
move r8 20 #a
L1:
"
}
);
Ok(())
}
#[test]
fn test_if_else_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let a = 0;
if (10 > 5) {
a = 1;
} else {
a = 2;
}
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 0 #a
sgt r1 10 5
beq r1 0 L2
move r8 1 #a
j L1
L2:
move r8 2 #a
L1:
"
}
);
Ok(())
}
#[test]
fn test_if_else_if_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let a = 0;
if (a == 1) {
a = 10;
} else if (a == 2) {
a = 20;
} else {
a = 30;
}
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 0 #a
seq r1 r8 1
beq r1 0 L2
move r8 10 #a
j L1
L2:
seq r2 r8 2
beq r2 0 L4
move r8 20 #a
j L3
L4:
move r8 30 #a
L3:
L1:
"
}
);
Ok(())
}
#[test]
fn test_spilled_variable_update_in_branch() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let a = 1;
let b = 2;
let c = 3;
let d = 4;
let e = 5;
let f = 6;
let g = 7;
let h = 8; // Spilled to stack (offset 0)
if (a == 1) {
h = 99;
}
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 1 #a
move r9 2 #b
move r10 3 #c
move r11 4 #d
move r12 5 #e
move r13 6 #f
move r14 7 #g
push 8 #h
seq r1 r8 1
beq r1 0 L1
sub r0 sp 1
put db r0 99 #h
L1:
"
}
);
Ok(())
}

View File

@@ -0,0 +1,243 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn no_arguments() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
fn doSomething() {};
let i = doSomething();
"
};
let to_test = indoc! {
"
j main
doSomething:
push ra
sub r0 sp 1
get ra db r0
sub sp sp 1
j ra
main:
jal doSomething
move r8 r15 #i
"
};
assert_eq!(compiled, to_test);
Ok(())
}
#[test]
fn let_var_args() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
fn doSomething(arg1) {};
let arg1 = 123;
let i = doSomething(arg1);
"
};
assert_eq!(
compiled,
indoc! {
"
j main
doSomething:
pop r8 #arg1
push ra
sub r0 sp 1
get ra db r0
sub sp sp 1
j ra
main:
move r8 123 #arg1
push r8
push r8
jal doSomething
sub r0 sp 1
get r8 db r0
sub sp sp 1
move r9 r15 #i
"
}
);
Ok(())
}
#[test]
fn incorrect_args_count() -> anyhow::Result<()> {
let compiled = compile! {
result
"
fn doSomething(arg1, arg2){};
let i = doSomething();
"
};
assert!(matches!(
compiled,
Err(super::super::Error::AgrumentMismatch(_))
));
Ok(())
}
#[test]
fn inline_literal_args() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
fn doSomething(arg1, arg2) {};
let thisVariableShouldStayInPlace = 123;
let returnedValue = doSomething(12, 34);
"
};
assert_eq!(
compiled,
indoc! {
"
j main
doSomething:
pop r8 #arg2
pop r9 #arg1
push ra
sub r0 sp 1
get ra db r0
sub sp sp 1
j ra
main:
move r8 123 #thisVariableShouldStayInPlace
push r8
push 12
push 34
jal doSomething
sub r0 sp 1
get r8 db r0
sub sp sp 1
move r9 r15 #returnedValue
"
}
);
Ok(())
}
#[test]
fn mixed_args() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let arg1 = 123;
let returnValue = doSomething(arg1, 456);
fn doSomething(arg1, arg2) {};
"
};
assert_eq!(
compiled,
indoc! {
"
j main
doSomething:
pop r8 #arg2
pop r9 #arg1
push ra
sub r0 sp 1
get ra db r0
sub sp sp 1
j ra
main:
move r8 123 #arg1
push r8
push r8
push 456
jal doSomething
sub r0 sp 1
get r8 db r0
sub sp sp 1
move r9 r15 #returnValue
"
}
);
Ok(())
}
#[test]
fn with_return_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
fn doSomething(arg1) {
return 456;
};
let returned = doSomething(123);
"
};
assert_eq!(
compiled,
indoc! {
"
j main
doSomething:
pop r8 #arg1
push ra
move r15 456 #returnValue
sub r0 sp 1
get ra db r0
sub sp sp 1
j ra
main:
push 123
jal doSomething
move r8 r15 #returned
"
}
);
Ok(())
}
#[test]
fn with_negative_return_literal() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
fn doSomething() {
return -1;
};
let i = doSomething();
"
};
assert_eq!(
compiled,
indoc! {
"
j main
doSomething:
push ra
move r15 -1 #returnValue
sub r0 sp 1
get ra db r0
sub sp sp 1
j ra
main:
jal doSomething
move r8 r15 #i
"
}
);
Ok(())
}

View File

@@ -0,0 +1,147 @@
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn variable_declaration_numeric_literal() -> anyhow::Result<()> {
let compiled = crate::compile! {
debug r#"
let i = 20c;
"#
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 293.15 #i
"
}
);
Ok(())
}
#[test]
fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()> {
let compiled = compile! {
debug
r#"
let a = 0;
let b = 1;
let c = 2;
let d = 3;
let e = 4;
let f = 5;
let g = 6;
let h = 7;
let i = 8;
let j = 9;
"#};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 0 #a
move r9 1 #b
move r10 2 #c
move r11 3 #d
move r12 4 #e
move r13 5 #f
move r14 6 #g
push 7 #h
push 8 #i
push 9 #j
"
}
);
Ok(())
}
#[test]
fn variable_declaration_negative() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let i = -1;
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 -1 #i
"
}
);
Ok(())
}
#[test]
fn test_boolean_declaration() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let t = true;
let f = false;
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 1 #t
move r9 0 #f
"
}
);
Ok(())
}
#[test]
fn test_boolean_return() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
fn getTrue() {
return true;
};
let val = getTrue();
"
};
assert_eq!(
compiled,
indoc! {
"
j main
getTrue:
push ra
move r15 1 #returnValue
sub r0 sp 1
get ra db r0
sub sp sp 1
j ra
main:
jal getTrue
move r8 r15 #val
"
}
);
Ok(())
}

View File

@@ -0,0 +1,58 @@
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> {
let compiled = compile!(debug r#"
// we need more than 4 params to 'spill' into a stack var
fn doSomething(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) {};
"#);
assert_eq!(
compiled,
indoc! {"
j main
doSomething:
pop r8 #arg9
pop r9 #arg8
pop r10 #arg7
pop r11 #arg6
pop r12 #arg5
pop r13 #arg4
pop r14 #arg3
push ra
sub r0 sp 1
get ra db r0
sub sp sp 3
j ra
"}
);
Ok(())
}
#[test]
fn test_function_declaration_with_register_params() -> anyhow::Result<()> {
let compiled = compile!(debug r#"
// This is a test function declaration with no body
fn doSomething(arg1, arg2) {
};
"#);
assert_eq!(
compiled,
indoc! {"
j main
doSomething:
pop r8 #arg2
pop r9 #arg1
push ra
sub r0 sp 1
get ra db r0
sub sp sp 1
j ra
"}
);
Ok(())
}

View File

@@ -0,0 +1,177 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_comparison_expressions() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let isGreater = 10 > 5;
let isLess = 5 < 10;
let isEqual = 5 == 5;
let isNotEqual = 5 != 10;
let isGreaterOrEqual = 10 >= 10;
let isLessOrEqual = 5 <= 5;
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
sgt r1 10 5
move r8 r1 #isGreater
slt r2 5 10
move r9 r2 #isLess
seq r3 5 5
move r10 r3 #isEqual
sne r4 5 10
move r11 r4 #isNotEqual
sge r5 10 10
move r12 r5 #isGreaterOrEqual
sle r6 5 5
move r13 r6 #isLessOrEqual
"
}
);
Ok(())
}
#[test]
fn test_logical_and_or_not() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let logic1 = 1 && 1;
let logic2 = 1 || 0;
let logic3 = !1;
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
and r1 1 1
move r8 r1 #logic1
or r2 1 0
move r9 r2 #logic2
seq r3 1 0
move r10 r3 #logic3
"
}
);
Ok(())
}
#[test]
fn test_complex_logic() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let logic = (10 > 5) && (5 < 10);
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
sgt r1 10 5
slt r2 5 10
and r3 r1 r2
move r8 r3 #logic
"
}
);
Ok(())
}
#[test]
fn test_math_with_logic() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let logic = (1 + 2) > 1;
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
add r1 1 2
sgt r2 r1 1
move r8 r2 #logic
"
}
);
Ok(())
}
#[test]
fn test_boolean_in_logic() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let res = true && false;
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
and r1 1 0
move r8 r1 #res
"
}
);
Ok(())
}
#[test]
fn test_invert_a_boolean() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let i = true;
let y = !i;
let result = y == false;
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 1 #i
seq r1 r8 0
move r9 r1 #y
seq r2 r9 0
move r10 r2 #result
"
}
);
Ok(())
}

View File

@@ -0,0 +1,149 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_infinite_loop() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let a = 0;
loop {
a = a + 1;
}
"
};
// Labels: L1 (start), L2 (end)
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 0 #a
L1:
add r1 r8 1
move r8 r1 #a
j L1
L2:
"
}
);
Ok(())
}
#[test]
fn test_loop_break() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let a = 0;
loop {
a = a + 1;
if (a > 10) {
break;
}
}
"
};
// Labels: L1 (start), L2 (end), L3 (if end - implicit else label)
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 0 #a
L1:
add r1 r8 1
move r8 r1 #a
sgt r2 r8 10
beq r2 0 L3
j L2
L3:
j L1
L2:
"
}
);
Ok(())
}
#[test]
fn test_while_loop() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
let a = 0;
while (a < 10) {
a = a + 1;
}
"
};
// Labels: L1 (start), L2 (end)
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 0 #a
L1:
slt r1 r8 10
beq r1 0 L2
add r2 r8 1
move r8 r2 #a
j L1
L2:
"
}
);
Ok(())
}
#[test]
fn test_loop_continue() -> anyhow::Result<()> {
let compiled = compile! {
debug
r#"
let a = 0;
loop {
a = a + 1;
if (a < 5) {
continue;
}
break;
}
"#
};
// Labels: L1 (start), L2 (end), L3 (if end)
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 0 #a
L1:
add r1 r8 1
move r8 r1 #a
slt r2 r8 5
beq r2 0 L3
j L1
L3:
j L2
j L1
L2:
"
}
);
Ok(())
}

View File

@@ -0,0 +1,50 @@
#![allow(clippy::crate_in_macro_def)]
macro_rules! output {
($input:expr) => {
String::from_utf8($input.into_inner()?)?
};
}
#[cfg_attr(test, macro_export)]
macro_rules! compile {
($source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = ::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))),
&mut writer,
None,
);
compiler.compile()?;
output!(writer)
}};
(result $source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = crate::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))),
&mut writer,
Some(crate::CompilerConfig { debug: true }),
);
compiler.compile()
}};
(debug $source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = crate::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))),
&mut writer,
Some(crate::CompilerConfig { debug: true }),
);
compiler.compile()?;
output!(writer)
}};
}
mod binary_expression;
mod branching;
mod declaration_function_invocation;
mod declaration_literal;
mod function_declaration;
mod logic_expression;
mod loops;
mod syscall;

View File

@@ -0,0 +1,159 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_yield() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
yield();
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
yield
"
}
);
Ok(())
}
#[test]
fn test_sleep() -> anyhow::Result<()> {
let compiled = compile! {
debug
"
sleep(3);
let sleepAmount = 15;
sleep(sleepAmount);
sleep(sleepAmount * 2);
"
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
sleep 3
move r8 15 #sleepAmount
sleep r8
mul r1 r8 2
sleep r1
"
}
);
Ok(())
}
#[test]
fn test_set_on_device() -> anyhow::Result<()> {
let compiled = compile! {
debug
r#"
device airConditioner = "d0";
let internalTemp = 20c;
setOnDevice(airConditioner, "On", internalTemp > 25c);
"#
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 293.15 #internalTemp
sgt r1 r8 298.15
s d0 On r1
"
}
);
Ok(())
}
#[test]
fn test_set_on_device_batched() -> anyhow::Result<()> {
let compiled = compile! {
debug
r#"
let doorHash = hash("Door");
setOnDeviceBatched(doorHash, "Lock", true);
"#
};
assert_eq!(
compiled,
indoc! {
r#"
j main
main:
move r15 HASH("Door") #hash_ret
move r8 r15 #doorHash
sb r8 Lock 1
"#
}
);
Ok(())
}
#[test]
fn test_load_from_device() -> anyhow::Result<()> {
let compiled = compile! {
debug
r#"
device airCon = "d0";
let setting = loadFromDevice(airCon, "On");
"#
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
l r15 d0 On
move r8 r15 #setting
"
}
);
Ok(())
}
#[test]
fn test_hash() -> anyhow::Result<()> {
let compiled = compile! {
debug
r#"
let nameHash = hash("testValue");
"#
};
assert_eq!(
compiled,
indoc! {
r#"
j main
main:
move r15 HASH("testValue") #hash_ret
move r8 r15 #nameHash
"#
}
);
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,188 @@
// r15 : Return Value
// r0 : Unmanaged temp variable
// r1 - r7 : Temporary Variables
// r8 - r14 : Persistant Variables
use quick_error::quick_error;
use std::collections::{HashMap, VecDeque};
const TEMP: [u8; 7] = [1, 2, 3, 4, 5, 6, 7];
const PERSIST: [u8; 7] = [8, 9, 10, 11, 12, 13, 14];
quick_error! {
#[derive(Debug)]
pub enum Error {
DuplicateVariable(var: String) {
display("{var} already exists.")
}
UnknownVariable(var: String) {
display("{var} does not exist.")
}
Unknown(reason: String) {
display("{reason}")
}
}
}
/// A request to store a variable at a specific register type
pub enum LocationRequest {
#[allow(dead_code)]
/// Request to store a variable in a temprary register.
Temp,
/// Request to store a variable in a persistant register.
Persist,
/// Request to store a variable in the stack.
Stack,
}
#[derive(Clone)]
pub enum VariableLocation {
/// Represents a temporary register (r1 - r7)
Temporary(u8),
/// Represents a persistant register (r8 - r14)
Persistant(u8),
/// Represents a a stack offset (current stack - offset = variable loc)
Stack(u16),
}
pub struct VariableScope<'a> {
temporary_vars: VecDeque<u8>,
persistant_vars: VecDeque<u8>,
var_lookup_table: HashMap<String, VariableLocation>,
stack_offset: u16,
parent: Option<&'a VariableScope<'a>>,
}
impl<'a> Default for VariableScope<'a> {
fn default() -> Self {
Self {
parent: None,
stack_offset: 0,
persistant_vars: PERSIST.to_vec().into(),
temporary_vars: TEMP.to_vec().into(),
var_lookup_table: HashMap::new(),
}
}
}
impl<'a> VariableScope<'a> {
#[allow(dead_code)]
pub const TEMP_REGISTER_COUNT: u8 = 7;
pub const PERSIST_REGISTER_COUNT: u8 = 7;
pub const RETURN_REGISTER: u8 = 15;
pub const TEMP_STACK_REGISTER: u8 = 0;
pub fn registers(&self) -> impl Iterator<Item = &u8> {
self.var_lookup_table
.values()
.filter(|val| {
matches!(
val,
VariableLocation::Temporary(_) | VariableLocation::Persistant(_)
)
})
.map(|loc| match loc {
VariableLocation::Persistant(reg) | VariableLocation::Temporary(reg) => reg,
_ => unreachable!(),
})
}
pub fn scoped(parent: &'a VariableScope<'a>) -> Self {
Self {
parent: Option::Some(parent),
..Default::default()
}
}
pub fn stack_offset(&self) -> u16 {
self.stack_offset
}
/// Adds and tracks a new scoped variable. If the location you request is full, will fall back
/// to the stack.
pub fn add_variable(
&mut self,
var_name: impl Into<String>,
location: LocationRequest,
) -> Result<VariableLocation, Error> {
let var_name = var_name.into();
if self.var_lookup_table.contains_key(var_name.as_str()) {
return Err(Error::DuplicateVariable(var_name));
}
let var_location = match location {
LocationRequest::Temp => {
if let Some(next_var) = self.temporary_vars.pop_front() {
VariableLocation::Temporary(next_var)
} else {
let loc = VariableLocation::Stack(self.stack_offset);
self.stack_offset += 1;
loc
}
}
LocationRequest::Persist => {
if let Some(next_var) = self.persistant_vars.pop_front() {
VariableLocation::Persistant(next_var)
} else {
let loc = VariableLocation::Stack(self.stack_offset);
self.stack_offset += 1;
loc
}
}
LocationRequest::Stack => {
let loc = VariableLocation::Stack(self.stack_offset);
self.stack_offset += 1;
loc
}
};
self.var_lookup_table.insert(var_name, var_location.clone());
Ok(var_location)
}
pub fn get_location_of(
&mut self,
var_name: impl Into<String>,
) -> Result<VariableLocation, Error> {
let var_name = var_name.into();
let var = self
.var_lookup_table
.get(var_name.as_str())
.cloned()
.ok_or(Error::UnknownVariable(var_name))?;
if let VariableLocation::Stack(inserted_at_offset) = var {
Ok(VariableLocation::Stack(
self.stack_offset - inserted_at_offset,
))
} else {
Ok(var)
}
}
pub fn has_parent(&self) -> bool {
self.parent.is_some()
}
#[allow(dead_code)]
pub fn free_temp(&mut self, var_name: impl Into<String>) -> Result<(), Error> {
let var_name = var_name.into();
let Some(location) = self.var_lookup_table.remove(var_name.as_str()) else {
return Err(Error::UnknownVariable(var_name));
};
match location {
VariableLocation::Temporary(t) => {
self.temporary_vars.push_back(t);
}
VariableLocation::Persistant(_) => {
return Err(Error::UnknownVariable(String::from(
"Attempted to free a `let` variable.",
)));
}
VariableLocation::Stack(_) => {}
};
Ok(())
}
}