[rlox] Implement Print Statements

This commit is contained in:
ctsk
2024-08-30 13:10:27 +02:00
parent 78ef9fdc07
commit 80b0c20ccd
5 changed files with 277 additions and 119 deletions

View File

@@ -1,8 +1,8 @@
use crate::gc::{GcHandle, Object};
use crate::gc::{GcHandle, Object, ObjectType};
use std::collections::LinkedList;
use std::convert::From;
use std::fmt;
use std::fmt::Debug;
use std::fmt::{self, Display};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Op {
@@ -20,6 +20,8 @@ pub enum Op {
Equal,
Greater,
Less,
Print,
}
#[derive(Clone, Debug, PartialEq)]
@@ -71,6 +73,28 @@ impl From<Object> for Value {
}
}
impl Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Nil => write!(f, "nil"),
Value::Bool(true) => write!(f, "true"),
Value::Bool(false) => write!(f, "false"),
Value::Number(number) => {
let stringified = number.to_string();
match stringified.strip_suffix(".0") {
Some(integer) => write!(f, "{}", integer),
None => write!(f, "{}", stringified),
}
}
Value::Obj(object) => match object.get_otype() {
ObjectType::String => {
write!(f, "{}", object)
}
},
}
}
}
pub struct Chunk {
pub code: Vec<Op>,
pub debug_info: Vec<usize>,
@@ -84,16 +108,21 @@ impl Chunk {
code: Vec::new(),
debug_info: Vec::new(),
constants: Vec::new(),
allocations: LinkedList::new()
allocations: LinkedList::new(),
}
}
pub fn new_with(code: Vec<Op>, debug_info: Vec<usize>, constants: Vec<Value>, allocations: LinkedList<GcHandle>) -> Self {
pub fn new_with(
code: Vec<Op>,
debug_info: Vec<usize>,
constants: Vec<Value>,
allocations: LinkedList<GcHandle>,
) -> Self {
Chunk {
code,
debug_info,
constants,
allocations
allocations,
}
}
@@ -179,7 +208,7 @@ impl fmt::Debug for TraceInfo<'_> {
.finish()?;
write!(f, "")
}
_ => write!(f, "{:?}", op)
_ => write!(f, "{:?}", op),
}
}
}
@@ -188,8 +217,8 @@ mod tests {
#[test]
fn string_value_equality() {
use crate::gc::allocate_string;
use crate::bc::Value;
use crate::gc::allocate_string;
let s1 = "bla5";
let s2 = "bla6";

View File

@@ -1,4 +1,7 @@
use std::{alloc::{alloc, dealloc, Layout, LayoutError}, fmt};
use std::{
alloc::{alloc, dealloc, Layout, LayoutError},
fmt::{self, Display},
};
#[derive(PartialEq, Eq, Clone, Copy)]
#[repr(usize)]
@@ -32,13 +35,20 @@ pub struct Object {
ptr: *mut ObjectHeader,
}
impl Display for Object {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.get_otype() {
ObjectType::String => {
write!(f, "{}", ObjString::as_str(self.ptr as *mut ObjStringHeader))
}
}
}
}
impl Object {
pub fn get_otype(&self) -> ObjectType {
unsafe {
(*self.ptr).otype
unsafe { (*self.ptr).otype }
}
}
}
impl fmt::Debug for Object {
@@ -46,14 +56,10 @@ impl fmt::Debug for Object {
match self.get_otype() {
ObjectType::String => {
let string = self.ptr as *mut ObjStringHeader;
let data: &[u8] = ObjString::as_slice(string);
write!(
f,
"STR {} {:?}",
data.len(),
&data[..8.min(data.len())],
)
},
let data = ObjString::as_str(string);
write!(f, "STR {} {:?}", data.len(), &data[..8.min(data.len())],)
}
}
}
}
@@ -78,11 +84,11 @@ impl PartialEq for Object {
return false;
}
let slice = ObjString::as_slice(header);
let other_slice = ObjString::as_slice(other_header);
let slice = ObjString::as_str(header);
let other_slice = ObjString::as_str(other_header);
slice == other_slice
},
}
}
}
}
@@ -101,24 +107,29 @@ impl ObjString {
Ok((layout.pad_to_align(), offset))
}
fn as_slice<'a>(ptr: *mut ObjStringHeader) -> &'a [u8] {
fn as_bytes<'a>(ptr: *const ObjStringHeader) -> &'a [u8] {
unsafe {
std::slice::from_raw_parts(
(ptr as *mut u8).offset(data_offset() as isize),
(*ptr).len
)
std::slice::from_raw_parts((ptr as *mut u8).offset(data_offset() as isize), (*ptr).len)
}
}
fn as_str<'a>(ptr: *const ObjStringHeader) -> &'a str {
unsafe { std::str::from_utf8_unchecked(ObjString::as_bytes(ptr)) }
}
}
pub unsafe fn allocate_string_obj<'a>(length: usize) -> Result<(GcHandle, &'a mut [u8]), LayoutError> {
pub unsafe fn allocate_string_obj<'a>(
length: usize,
) -> Result<(GcHandle, &'a mut [u8]), LayoutError> {
let (layout, offset) = ObjString::layout(length)?;
let allocation = alloc(layout);
let data_ptr = allocation.offset(offset as isize);
let header = allocation as *mut ObjStringHeader;
(*header).len = length;
(*header).object_header.otype = ObjectType::String;
let object = Object { ptr: header as *mut ObjectHeader };
let object = Object {
ptr: header as *mut ObjectHeader,
};
let str = std::slice::from_raw_parts_mut(data_ptr, length);
Ok((GcHandle { object }, str))
}
@@ -132,8 +143,8 @@ pub unsafe fn allocate_string(content: &str) -> Result<GcHandle, LayoutError> {
pub unsafe fn concat_string(a: Object, b: Object) -> Result<GcHandle, LayoutError> {
let a_head = a.ptr as *mut ObjStringHeader;
let b_head = b.ptr as *mut ObjStringHeader;
let a_data = ObjString::as_slice(a_head);
let b_data = ObjString::as_slice(b_head);
let a_data = ObjString::as_bytes(a_head);
let b_data = ObjString::as_bytes(b_head);
let new_len = a_data.len() + b_data.len();
let (gc_handle, slice) = allocate_string_obj(new_len)?;
@@ -158,7 +169,7 @@ unsafe fn deallocate_object(object: Object) {
#[derive(Debug, Clone, PartialEq)]
pub struct GcHandle {
object: Object
object: Object,
}
impl Drop for GcHandle {

View File

@@ -1,6 +1,6 @@
use std::{collections::HashMap, convert::identity};
use std::iter::Peekable;
use std::str::CharIndices;
use std::{collections::HashMap, convert::identity};
use crate::bc::{Chunk, Op};
use crate::gc::allocate_string;
@@ -234,7 +234,10 @@ impl<'src> Iterator for Scanner<'src> {
'+' => make_simple_token(self, TokenType::Plus),
';' => make_simple_token(self, TokenType::Semicolon),
'/' => match self.consume_if_eq('/') {
Some(_) => { self.scan_comment(); self.next() },
Some(_) => {
self.scan_comment();
self.next()
}
None => make_simple_token(self, TokenType::Slash),
},
'*' => make_simple_token(self, TokenType::Star),
@@ -288,7 +291,14 @@ enum Precedence {
Primary,
}
enum ParseError<'src> {
InvalidNumber(Token<'src>)
}
type Result<'src, T> = std::result::Result<T, ParseError<'src>>;
impl<'src> Parser<'src> {
fn new(sc: Scanner<'src>) -> Self {
Parser {
scanner: sc.into_iter().peekable(),
@@ -330,50 +340,50 @@ impl<'src> Parser<'src> {
_ => unreachable!(),
};
chunk.add_op(op, 0);
},
}
TokenType::Number => {
match token.span.parse::<f64>() {
Ok(c) => chunk.add_constant(c.into(), 0),
_ => panic!("Could not parse number"),
};
},
}
TokenType::String => {
let without_quotes = &token.span[1..(token.span.len() - 1)];
match self.intern_table.get(without_quotes) {
Some(&index) => {
chunk.add_op(
Op::Constant {
offset: index,
},
0
);
},
chunk.add_op(Op::Constant { offset: index }, 0);
}
None => {
let object = unsafe { allocate_string(without_quotes) }.unwrap();
chunk.add_constant(object.get_object().into(), 0);
self.intern_table.insert(without_quotes, chunk.constants.len() as u8 - 1);
self.intern_table
.insert(without_quotes, chunk.constants.len() as u8 - 1);
chunk.allocations.push_front(object);
},
}
};
},
}
TokenType::Nil | TokenType::True | TokenType::False => {
let op = match token.ttype {
TokenType::Nil => Op::Nil,
TokenType::True => Op::True,
TokenType::False => Op::False,
_ => unreachable!()
_ => unreachable!(),
};
chunk.add_op(op, 0);
},
}
TokenType::LeftParen => {
self._expression(chunk, Precedence::None);
assert_eq!(self.scanner.next().unwrap().ttype, TokenType::RightParen)
},
}
_ => panic!("Expected '-' or number"),
},
};
while let Some(op) = self.scanner.next_if(|token| {
if token.ttype == TokenType::Semicolon {
return false;
}
let op_prec = Self::precedence(token.ttype);
if op_prec == min_prec {
match Self::associativity(min_prec) {
@@ -404,17 +414,62 @@ impl<'src> Parser<'src> {
_ => todo!(),
};
}
}
pub fn expression(&mut self, chunk: &mut Chunk) {
self._expression(chunk, Precedence::None)
}
pub fn must_consume(&mut self, ttype: TokenType) -> Option<Token> {
self.scanner.next_if(
|tok| tok.ttype == ttype
).or_else(
|| panic!()
)
}
pub fn print_statement(&mut self, chunk: &mut Chunk) {
self.must_consume(TokenType::Print).unwrap();
self.expression(chunk);
chunk.add_op(Op::Print, 0);
self.must_consume(TokenType::Semicolon).unwrap();
}
pub fn expr_statement(&mut self, chunk: &mut Chunk) {
self.expression(chunk);
self.must_consume(TokenType::Semicolon).unwrap();
}
pub fn statement(&mut self, chunk: &mut Chunk) {
match self.scanner.peek().unwrap().ttype {
TokenType::Print => self.print_statement(chunk),
_ => self.expr_statement(chunk),
}
}
pub fn declaration(&mut self, chunk: &mut Chunk) {
self.statement(chunk);
}
pub fn compile(&mut self, chunk: &mut Chunk) {
while let Some(_) = self.scanner.peek() {
self.declaration(chunk);
}
}
}
#[cfg(test)]
pub fn compile_expr(source: &str, chunk: &mut Chunk) {
let scanner = Scanner::new(source);
let mut parser = Parser::new(scanner);
parser.expression(chunk);
}
pub fn compile(source: &str, chunk: &mut Chunk) {
let scanner = Scanner::new(source);
let mut parser = Parser::new(scanner);
parser.expression(chunk);
parser.compile(chunk);
}
#[cfg(test)]
@@ -508,12 +563,10 @@ mod tests {
assert_eq!(
tokens,
vec![
Token {
vec![Token {
ttype: TokenType::String,
span: &source[0..=12]
}
]
}]
);
assert_eq!(tokens[0].span, source);
@@ -527,6 +580,14 @@ mod tests {
assert!(chunk.instr_eq(expected));
}
fn test_parse_program(source: &str, expected: &Chunk) {
let scanner = Scanner::new(source);
let mut parser = Parser::new(scanner);
let mut chunk = Chunk::new();
parser.compile(&mut chunk);
assert!(chunk.instr_eq(expected));
}
#[test]
fn test_parser() {
let source = "1 + 1 * (2 + 1)";
@@ -553,12 +614,7 @@ mod tests {
fn parse_nil() {
let source = "nil + nil";
use crate::bc::Op::*;
let expected = Chunk::new_with(
vec![Nil, Nil, Add],
vec![],
vec![],
LinkedList::new(),
);
let expected = Chunk::new_with(vec![Nil, Nil, Add], vec![], vec![], LinkedList::new());
test_parse_expression(source, &expected);
}
@@ -583,12 +639,9 @@ mod tests {
use crate::bc::Op::*;
let expected = Chunk::new_with(
vec![
False, Not, True, Not, True, Less, Not,
False, Greater, Not,
True, Greater,
False, Less,
Equal,
True, Equal, Not],
False, Not, True, Not, True, Less, Not, False, Greater, Not, True, Greater, False,
Less, Equal, True, Equal, Not,
],
vec![],
vec![],
LinkedList::new(),
@@ -609,4 +662,49 @@ mod tests {
assert_eq!(chunk.constants.len(), 1);
}
#[test]
fn basic_print_statement() {
let source = "print 1 + 1;";
use crate::bc::Op::*;
let expected = Chunk::new_with(
vec![Constant { offset: 0 }, Constant { offset: 1 }, Add, Print],
vec![],
vec![Value::from(1.0), Value::from(1.0)],
LinkedList::new(),
);
test_parse_program(source, &expected);
}
#[test]
fn basic_print_string_statement() {
let source = "print \"string\";";
let allocation = unsafe { allocate_string("string").unwrap() };
let object = allocation.get_object();
let mut allocations = LinkedList::new();
allocations.push_front(allocation);
use crate::bc::Op::*;
let expected = Chunk::new_with(
vec![Constant { offset: 0 }, Print],
vec![],
vec![Value::from(object)],
allocations,
);
test_parse_program(source, &expected);
}
#[test]
fn basic_expr_statement() {
let source = "1 / 1;";
use crate::bc::Op::*;
let expected = Chunk::new_with(
vec![Constant { offset: 0 }, Constant { offset: 1 }, Divide],
vec![],
vec![Value::from(1.0), Value::from(1.0)],
LinkedList::new(),
);
test_parse_program(source, &expected);
}
}

View File

@@ -9,6 +9,7 @@ use std::io;
use bc::Chunk;
use vm::VM;
fn repl() {
let mut buffer = String::new();
@@ -21,7 +22,7 @@ fn repl() {
lc::compile(buffer.as_str(), &mut chunk);
let mut vm = VM::new();
vm.set_trace(do_trace);
let result = vm.run(&chunk);
let result = vm.stdrun(&chunk);
println!("{:?}", result);
buffer.clear();
}
@@ -49,26 +50,40 @@ fn main() {
#[cfg(test)]
mod tests {
use crate::{bc::{Chunk, Value}, gc::allocate_string, lc::compile, vm::VM};
use std::io::BufWriter;
use crate::{bc::{Chunk, Value}, gc::allocate_string, lc::{compile, compile_expr}, vm::VM};
#[test]
fn test_compile_and_run_pi_math() {
let source = "-(3 * 7 * 11 * 17) / -(500 + 1000 - 250)";
let mut chunk = Chunk::new();
compile(source, &mut chunk);
compile_expr(source, &mut chunk);
let mut vm = VM::new();
vm.run(&chunk).unwrap();
vm.stdrun(&chunk).unwrap();
}
#[test]
fn string_concatenation() {
let source = "\"hello\" + \" \" + \"world\"";
let mut chunk = Chunk::new();
compile(source, &mut chunk);
compile_expr(source, &mut chunk);
let mut vm = VM::new();
let (result, _allocs) = vm.run(&chunk).unwrap().unwrap();
let (result, _allocs) = vm.stdrun(&chunk).unwrap().unwrap();
let target_alloc = unsafe { allocate_string("hello world").unwrap() };
let target = Value::from(target_alloc.get_object());
assert_eq!(result, target);
}
#[test]
fn print_hello_world() {
let source = "print \"hello\" + \" \" + \"world\";";
let mut chunk = Chunk::new();
let mut vm = VM::new();
compile(source, &mut chunk);
let mut buf = BufWriter::new(Vec::new());
vm.run(&chunk, &mut buf).unwrap();
let stdoutput = String::from_utf8(buf.into_inner().unwrap()).unwrap();
assert_eq!(stdoutput, "hello world\n");
}
}

View File

@@ -1,6 +1,7 @@
use crate::bc::{Chunk, Op, TraceInfo, Value};
use crate::gc::{concat_string, GcHandle, ObjectType};
use std::collections::LinkedList;
use std::io;
use std::rc::Rc;
pub struct VM {
@@ -56,7 +57,18 @@ impl VM {
.ok_or(self.type_err("Number", top_of_stack))
}
pub fn run(&mut self, chunk: &Chunk) -> Result<Option<(Value, LinkedList<GcHandle>)>, VMError> {
pub fn stdrun(
&mut self,
chunk: &Chunk,
) -> Result<Option<(Value, LinkedList<GcHandle>)>, VMError> {
return self.run(chunk, &mut io::stdout());
}
pub fn run<Output: io::Write>(
&mut self,
chunk: &Chunk,
output: &mut Output,
) -> Result<Option<(Value, LinkedList<GcHandle>)>, VMError> {
let mut allocations: LinkedList<GcHandle> = LinkedList::new();
while self.pc < chunk.code.len() {
@@ -106,31 +118,28 @@ impl VM {
let a = self.pop_num()?;
self.push(Value::from(num + a));
}
Value::Obj(b) => {
match b.get_otype() {
Value::Obj(b) => match b.get_otype() {
ObjectType::String => {
let a = self.pop()?;
match a {
Value::Obj(a) => {
match a.get_otype() {
Value::Obj(a) => match a.get_otype() {
ObjectType::String => {
let new_obj = unsafe {
concat_string(a, b).unwrap()
};
let new_obj = unsafe { concat_string(a, b).unwrap() };
self.push(Value::from(new_obj.get_object()));
allocations.push_front(new_obj);
Ok(())
}
},
_ => Err(self.type_err("String", a)),
}?
}
},
_ => {
Err(self.type_err("String", a))
return Err(VMError::Runtime(
"Operands of + need to be numbers or strings".into(),
self.pc,
))
}
}?
},
}
}
_ => return Err(VMError::Runtime("Operands of + need to be numbers or strings".into(), self.pc))
};
}
Op::Subtract | Op::Multiply | Op::Divide => {
@@ -160,15 +169,21 @@ impl VM {
let r = a == b;
self.push(r.into())
}
Op::Print => {
let value = self.pop()?;
writeln!(output, "{}", value)
.map_err(|_| VMError::Runtime("Failed to print".into(), self.pc))?
}
}
}
match self.stack.pop() {
None => Ok(None),
Some(result_value) => {
let escaping_allocs = allocations.into_iter().filter(
|handle| Value::from(handle.get_object()) == result_value
).collect();
let escaping_allocs = allocations
.into_iter()
.filter(|handle| Value::from(handle.get_object()) == result_value)
.collect();
Ok(Some((result_value, escaping_allocs)))
}
@@ -211,7 +226,7 @@ mod tests {
);
let mut vm = VM::new();
let (result, allocs) = vm.run(&chunk).unwrap().unwrap();
let (result, allocs) = vm.stdrun(&chunk).unwrap().unwrap();
assert_eq!(result, Value::from(3.1416));
assert!(vm.stack.is_empty());
@@ -220,16 +235,11 @@ mod tests {
#[test]
fn nil_error() {
let chunk = Chunk::new_with(
vec![Op::Nil, Op::Negate],
vec![],
vec![],
LinkedList::new(),
);
let chunk = Chunk::new_with(vec![Op::Nil, Op::Negate], vec![], vec![], LinkedList::new());
let mut vm = VM::new();
assert_eq!(
vm.run(&chunk).unwrap_err(),
vm.stdrun(&chunk).unwrap_err(),
vm.type_err("Number", Value::Nil)
);
}
@@ -243,7 +253,7 @@ mod tests {
LinkedList::new(),
);
let mut vm = VM::new();
let (result, allocs) = vm.run(&chunk)?.unwrap();
let (result, allocs) = vm.stdrun(&chunk)?.unwrap();
assert_eq!(result, true.into());
assert!(vm.stack.is_empty());
@@ -254,14 +264,9 @@ mod tests {
#[test]
fn not_nil_is_true() {
let chunk = Chunk::new_with(
vec![Op::Nil, Op::Not],
vec![],
vec![],
LinkedList::new(),
);
let chunk = Chunk::new_with(vec![Op::Nil, Op::Not], vec![], vec![], LinkedList::new());
let mut vm = VM::new();
let (result, allocs) = vm.run(&chunk).unwrap().unwrap();
let (result, allocs) = vm.stdrun(&chunk).unwrap().unwrap();
assert_eq!(result, true.into());
assert!(vm.stack.is_empty());