diff --git a/rust/.gitignore b/rust/.gitignore
new file mode 100644
index 0000000..eb5a316
--- /dev/null
+++ b/rust/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
new file mode 100644
index 0000000..661ac0d
--- /dev/null
+++ b/rust/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "lox"
+version = "0.1.0"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
new file mode 100644
index 0000000..f6e02e9
--- /dev/null
+++ b/rust/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "lox"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
diff --git a/rust/justfile b/rust/justfile
new file mode 100644
index 0000000..cdac729
--- /dev/null
+++ b/rust/justfile
@@ -0,0 +1,2 @@
+test-watch:
+ while true; do fd .*.rs | entr -d cargo test; done
diff --git a/rust/src/chunk.rs b/rust/src/chunk.rs
new file mode 100644
index 0000000..66ff7f8
--- /dev/null
+++ b/rust/src/chunk.rs
@@ -0,0 +1,112 @@
+use std::alloc::{self, Layout};
+use std::marker::PhantomData;
+use std::ptr::{self, NonNull};
+
+pub enum OpCode {
+ Return,
+}
+
+pub struct Chunk {
+ count: usize,
+ capacity: usize,
+ code: NonNull<u8>,
+ _marker: PhantomData<u8>,
+}
+
+unsafe impl Send for Chunk {}
+unsafe impl Sync for Chunk {}
+
+impl Chunk {
+ pub fn new() -> Self {
+ Chunk {
+ count: 0,
+ capacity: 0,
+ code: NonNull::dangling(),
+ _marker: PhantomData,
+ }
+ }
+
+ // https://doc.rust-lang.org/nomicon/vec/vec-push-pop.html
+ pub fn write(&mut self, byte: u8) {
+ if self.count == self.capacity {
+ self.grow();
+ }
+
+ unsafe {
+ ptr::write(self.code.as_ptr().add(self.count), byte);
+ }
+
+ // Can't fail, we'll OOM first.
+ self.count += 1;
+ }
+
+ // https://doc.rust-lang.org/nomicon/vec/vec-alloc.html
+ fn grow(&mut self) {
+ // Crafting Interpreters: GROW_CAPACITY
+ let new_cap = if self.capacity < 8 {
+ 8
+ } else {
+ // This can't overflow since self.capacity <= isize::MAX.
+ self.capacity * 2
+ };
+
+ // `Layout::array` checks that the number of bytes is <= usize::MAX,
+ // but this is redundant since old_layout.size() <= isize::MAX,
+ // so the `unwrap` should never fail.
+ let new_layout = Layout::array::<u8>(new_cap).unwrap();
+
+ // Ensure that the new allocation doesn't exceed `isize::MAX` bytes.
+ assert!(
+ new_layout.size() <= isize::MAX as usize,
+ "Allocation too large"
+ );
+
+ // Crafting Interpreters: GROW_ARRAY / reallocate
+ // TODO: Handle shrinking maybe? We may need to
+ // extract these out if it's used elsewhere
+ // in the book
+ let new_ptr = if self.capacity == 0 {
+ unsafe { alloc::alloc(new_layout) }
+ } else {
+ let old_layout = Layout::array::<u8>(self.capacity).unwrap();
+ let old_ptr = self.code.as_ptr() as *mut u8;
+ unsafe { alloc::realloc(old_ptr, old_layout, new_layout.size()) }
+ };
+
+ // If allocation fails, `new_ptr` will be null, in which case we abort.
+ self.code = match NonNull::new(new_ptr as *mut u8) {
+ Some(p) => p,
+ None => alloc::handle_alloc_error(new_layout),
+ };
+ self.capacity = new_cap;
+ }
+
+ // https://doc.rust-lang.org/nomicon/vec/vec-push-pop.html
+ pub fn pop(&mut self) -> Option<u8> {
+ if self.count == 0 {
+ None
+ } else {
+ self.count -= 1;
+ unsafe { Some(ptr::read(self.code.as_ptr().add(self.count))) }
+ }
+ }
+}
+
+// https://doc.rust-lang.org/nomicon/vec/vec-dealloc.html
+impl Drop for Chunk {
+ fn drop(&mut self) {
+ if self.capacity != 0 {
+ while self.pop().is_some() {}
+ let layout = Layout::array::<u8>(self.capacity).unwrap();
+ unsafe {
+ alloc::dealloc(self.code.as_ptr() as *mut u8, layout);
+ }
+ }
+ }
+}
+
+impl Default for Chunk {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/rust/src/main.rs b/rust/src/main.rs
new file mode 100644
index 0000000..88c5d67
--- /dev/null
+++ b/rust/src/main.rs
@@ -0,0 +1,5 @@
+pub mod chunk;
+
+fn main() {
+ println!("Hello, world!");
+}