Skip to content

Commit 221baf4

Browse files
committed
Add spawn/join for thread creation - wip
1 parent 6ce0740 commit 221baf4

File tree

4 files changed

+162
-23
lines changed

4 files changed

+162
-23
lines changed

README.md

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,6 @@ A Lisp interpreter with a register-based bytecode virtual machine.
1212
- **Immutable by default** - Values are immutable, inspired by FP
1313
- **Fast startup** - Minimal overhead, just feed the VM with code
1414

15-
## Installation
16-
17-
### Building from Source
18-
19-
```bash
20-
~$ git clone https://github.com/0xhenrique/code-reg.git
21-
~$ cd reg
22-
~$ cargo build --release
23-
```
24-
2515
## How to use
2616

2717
### Running a Lisp File
@@ -109,7 +99,7 @@ Options:
10999

110100
### Recursion
111101

112-
The VM supports kinda recursion with TCO. This is not battle tested:
102+
The VM kinda supports recursion with TCO. This is not battle tested:
113103

114104
```lisp
115105
; Regular recursion
@@ -261,13 +251,14 @@ Macros enable compile-time code generation:
261251

262252
The VM uses some common optimization techniques:
263253

264-
- **Compile-time constant folding**: Expressions like `(+ 1 2 3)` compile directly to `6`
265-
- **Pure function inlining**: Pure functions with constant arguments are evaluated at compile time
254+
- **Constant folding**: Expressions like `(+ 1 2 3)` compile directly to `6`
255+
- **Function inlining**: Pure functions with constant arguments are evaluated at compile time
266256
- **Register-based bytecode**: Reduced memory traffic
267257
- **Specialized opcodes**: Direct arithmetic operations bypass function call overhead
268258
- **Tail call optimization**: Recursive tail calls reuse stack frames
269259

270-
This is a rabbit hole by itself. I'm not expecting to beat something like Lua.
260+
Optimization is a rabbit hole by itself. I'm not expecting to beat something like Lua.
261+
For now, I will keep as is. Just don't try any memory intensive tests and it should work fine.
271262

272263
## Acknowledgments
273264

lisp/test_spawn.lisp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
;; Test spawn and join - Phase 9 Concurrency
2+
3+
(println "Testing spawn and join...")
4+
5+
;; Test 1: Simple spawn/join - just return a value
6+
(println "\n=== Test 1: Simple constant ===")
7+
(def worker1 (fn () 42))
8+
(def handle1 (spawn worker1))
9+
(def result1 (join handle1))
10+
(println "Result:" result1)
11+
12+
;; Test 2: Simple arithmetic
13+
(println "\n=== Test 2: Simple arithmetic ===")
14+
(def worker2 (fn () (+ (* 6 7) 10)))
15+
(def handle2 (spawn worker2))
16+
(def result2 (join handle2))
17+
(println "Result:" result2)
18+
19+
;; Test 3: Working with lists (built-in functions only)
20+
(println "\n=== Test 3: List operations ===")
21+
(def worker3 (fn ()
22+
(def nums (list 1 2 3 4 5))
23+
(def first-item (car nums))
24+
(def rest-items (cdr nums))
25+
(+ first-item (car rest-items))))
26+
(def handle3 (spawn worker3))
27+
(def result3 (join handle3))
28+
(println "Result:" result3)
29+
30+
;; Test 4: Multiple independent threads
31+
(println "\n=== Test 4: Three parallel computations ===")
32+
(def compute1 (fn () (+ 10 20 30)))
33+
(def compute2 (fn () (* 5 7)))
34+
(def compute3 (fn () (- 100 25)))
35+
36+
(def t1 (spawn compute1))
37+
(def t2 (spawn compute2))
38+
(def t3 (spawn compute3))
39+
40+
(println "Threads spawned, joining...")
41+
(def r1 (join t1))
42+
(def r2 (join t2))
43+
(def r3 (join t3))
44+
45+
(println "Thread 1:" r1)
46+
(println "Thread 2:" r2)
47+
(println "Thread 3:" r3)
48+
(println "Sum:" (+ r1 r2 r3))
49+
50+
;; Test 5: Thread returning a list
51+
(println "\n=== Test 5: Thread returning list ===")
52+
(def list-worker (fn ()
53+
(list 1 2 3 4 5)))
54+
(def list-handle (spawn list-worker))
55+
(def list-result (join list-handle))
56+
(println "List from thread:" list-result)
57+
(println "Length:" (length list-result))
58+
59+
(println "\n=== All spawn/join tests passed! ===")

src/value.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use std::cell::RefCell;
22
use std::collections::HashMap;
33
use std::fmt;
44
use std::rc::Rc;
5-
use std::sync::Arc;
5+
use std::sync::{Arc, Mutex};
6+
use std::thread::JoinHandle;
67

78
use crate::bytecode::Chunk;
89

@@ -260,6 +261,9 @@ pub enum HeapObject {
260261
NativeFunction(NativeFunction),
261262
/// Compiled function - keeps Rc<Chunk> for sharing in tail calls
262263
CompiledFunction(Rc<Chunk>),
264+
/// Thread handle for spawned threads - wrapped in Mutex because JoinHandle can only be joined once
265+
/// The Option allows us to take the handle when joining (join consumes the handle)
266+
ThreadHandle(Arc<Mutex<Option<JoinHandle<Result<SharedValue, String>>>>>),
263267
}
264268

265269
impl Clone for HeapObject {
@@ -272,6 +276,7 @@ impl Clone for HeapObject {
272276
HeapObject::Function(f) => HeapObject::Function(f.clone()),
273277
HeapObject::NativeFunction(f) => HeapObject::NativeFunction(f.clone()),
274278
HeapObject::CompiledFunction(c) => HeapObject::CompiledFunction(c.clone()),
279+
HeapObject::ThreadHandle(h) => HeapObject::ThreadHandle(h.clone()),
275280
}
276281
}
277282
}
@@ -297,6 +302,8 @@ pub struct SharedConsCell {
297302

298303
/// Thread-safe heap objects using Arc instead of Rc
299304
/// Mirrors HeapObject but is safe to send across threads
305+
/// Note: Functions are NOT included because closures capture environments with Rc
306+
/// and cannot be shared across threads. Use compiled functions or native functions instead.
300307
#[derive(Debug, Clone)]
301308
pub enum SharedHeapObject {
302309
/// String data
@@ -307,10 +314,6 @@ pub enum SharedHeapObject {
307314
List(Box<[SharedValue]>),
308315
/// Cons cell with shared values
309316
Cons(SharedConsCell),
310-
/// Functions cannot be shared across threads (closures capture environment)
311-
/// Use channels or other communication primitives instead
312-
/// We keep this variant for completeness but it will error if attempted
313-
Function(Box<Function>),
314317
/// Native functions are just function pointers, safe to share
315318
NativeFunction(NativeFunction),
316319
/// Compiled functions share their chunk via Arc
@@ -374,7 +377,6 @@ impl fmt::Display for SharedValue {
374377
}
375378
write!(f, ")")
376379
}
377-
SharedHeapObject::Function(_) => write!(f, "<function>"),
378380
SharedHeapObject::NativeFunction(nf) => write!(f, "<native fn {}>", nf.name),
379381
SharedHeapObject::CompiledFunction(_) => write!(f, "<function>"),
380382
}
@@ -496,7 +498,7 @@ impl Value {
496498

497499
/// Create a heap-allocated value from an Rc<HeapObject>
498500
/// Uses Rc::into_raw to store the pointer - refcount is NOT decremented
499-
fn from_heap(heap: Rc<HeapObject>) -> Value {
501+
pub fn from_heap(heap: Rc<HeapObject>) -> Value {
500502
let ptr = Rc::into_raw(heap) as u64;
501503
debug_assert!(ptr & TAG_MASK == 0, "Pointer uses more than 48 bits");
502504
Value(TAG_PTR | ptr)
@@ -597,7 +599,7 @@ impl Value {
597599

598600
/// Get the heap object if this is a heap pointer (Rc or arena)
599601
#[inline]
600-
fn as_heap(&self) -> Option<&HeapObject> {
602+
pub fn as_heap(&self) -> Option<&HeapObject> {
601603
if self.is_ptr() {
602604
let ptr = (self.0 & PAYLOAD_MASK) as *const HeapObject;
603605
// Safety: we only create these pointers from Rc::into_raw
@@ -776,6 +778,7 @@ impl Value {
776778
HeapObject::Function(_) => "function",
777779
HeapObject::NativeFunction(_) => "native-function",
778780
HeapObject::CompiledFunction(_) => "function",
781+
HeapObject::ThreadHandle(_) => "thread-handle",
779782
}
780783
} else {
781784
"unknown"
@@ -959,6 +962,11 @@ impl Value {
959962
Some(HeapObject::CompiledFunction(c)) => {
960963
Value::CompiledFunction(c.clone())
961964
}
965+
Some(HeapObject::ThreadHandle(h)) => {
966+
// ThreadHandles are already Arc-based, just clone the Value
967+
let heap = Rc::new(HeapObject::ThreadHandle(h.clone()));
968+
Value::from_heap(heap)
969+
}
962970
None => Value::nil(),
963971
}
964972
} else if self.is_ptr() {
@@ -1073,6 +1081,9 @@ impl Value {
10731081
inner: Arc::new(SharedHeapObject::CompiledFunction(arc_chunk)),
10741082
})
10751083
}
1084+
Some(HeapObject::ThreadHandle(_)) => {
1085+
Err("Thread handles cannot be shared across threads".to_string())
1086+
}
10761087
None => Err("Cannot convert unknown value to SharedValue".to_string()),
10771088
}
10781089
}
@@ -1122,7 +1133,6 @@ impl Value {
11221133
let cdr = Value::from_shared(&cons.cdr);
11231134
Value::cons_rc(car, cdr)
11241135
}
1125-
SharedHeapObject::Function(f) => Value::function((**f).clone()),
11261136
SharedHeapObject::NativeFunction(nf) => {
11271137
Value::native_function(&nf.name, nf.func)
11281138
}
@@ -1212,6 +1222,7 @@ impl fmt::Display for Value {
12121222
HeapObject::Function(_) => write!(f, "<function>"),
12131223
HeapObject::NativeFunction(nf) => write!(f, "<native fn {}>", nf.name),
12141224
HeapObject::CompiledFunction(_) => write!(f, "<function>"),
1225+
HeapObject::ThreadHandle(_) => write!(f, "<thread-handle>"),
12151226
}
12161227
} else {
12171228
write!(f, "<unknown>")

src/vm.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1960,6 +1960,84 @@ pub fn standard_vm() -> VM {
19601960
.map_err(|e| format!("write-file error: {}", e))
19611961
}));
19621962

1963+
vm.define_global("spawn", native("spawn", |args| {
1964+
use std::sync::{Arc, Mutex};
1965+
use std::thread;
1966+
1967+
if args.len() != 1 {
1968+
return Err("spawn expects 1 argument (function)".to_string());
1969+
}
1970+
1971+
// Get the function to spawn
1972+
let func = &args[0];
1973+
1974+
// Only compiled functions can be spawned (not closures with captured environments)
1975+
let chunk = if let Some(chunk) = func.as_compiled_function() {
1976+
chunk.clone()
1977+
} else {
1978+
return Err("spawn expects a compiled function (not a closure)".to_string());
1979+
};
1980+
1981+
// Convert Rc<Chunk> to Arc<Chunk> for thread safety
1982+
let arc_chunk = Arc::new((*chunk).clone());
1983+
1984+
// Spawn the thread
1985+
let handle = thread::spawn(move || {
1986+
// Create a new VM for this thread
1987+
let mut thread_vm = standard_vm();
1988+
1989+
// Execute the function (it should be zero-argument)
1990+
match thread_vm.run((*arc_chunk).clone()) {
1991+
Ok(result) => {
1992+
// Convert result to SharedValue
1993+
result.make_shared()
1994+
}
1995+
Err(e) => Err(format!("Thread execution error: {}", e)),
1996+
}
1997+
});
1998+
1999+
// Wrap the JoinHandle in Arc<Mutex<Option<>>> so it can be joined once
2000+
let thread_handle = Arc::new(Mutex::new(Some(handle)));
2001+
2002+
// Create a HeapObject::ThreadHandle and wrap it in a Value
2003+
let heap = Rc::new(crate::value::HeapObject::ThreadHandle(thread_handle));
2004+
Ok(Value::from_heap(heap))
2005+
}));
2006+
2007+
vm.define_global("join", native("join", |args| {
2008+
use crate::value::HeapObject;
2009+
2010+
if args.len() != 1 {
2011+
return Err("join expects 1 argument (thread-handle)".to_string());
2012+
}
2013+
2014+
// Get the thread handle
2015+
let handle_obj = args[0].as_heap()
2016+
.ok_or_else(|| "join expects a thread-handle".to_string())?;
2017+
2018+
if let HeapObject::ThreadHandle(handle_mutex) = handle_obj {
2019+
// Take the handle from the mutex (can only join once)
2020+
let handle_opt = handle_mutex.lock()
2021+
.map_err(|_| "Failed to lock thread handle".to_string())?
2022+
.take();
2023+
2024+
let handle = handle_opt
2025+
.ok_or_else(|| "Thread already joined".to_string())?;
2026+
2027+
// Wait for the thread to complete
2028+
let result = handle.join()
2029+
.map_err(|_| "Thread panicked".to_string())?;
2030+
2031+
// Convert the result back from Result<SharedValue, String> to Value
2032+
match result {
2033+
Ok(shared_value) => Ok(Value::from_shared(&shared_value)),
2034+
Err(e) => Err(format!("Thread error: {}", e)),
2035+
}
2036+
} else {
2037+
Err("join expects a thread-handle".to_string())
2038+
}
2039+
}));
2040+
19632041
vm
19642042
}
19652043

0 commit comments

Comments
 (0)