diff options
| author | Brian Cully <bjc@spork.org> | 2025-08-21 11:08:27 -0400 |
|---|---|---|
| committer | Brian Cully <bjc@spork.org> | 2025-08-21 12:33:12 -0400 |
| commit | c4b52f5f0770d3a83d67815b18e3cd76b01f0258 (patch) | |
| tree | 3dc982c60e7c1103af472ebdb515c358b51152e4 /src/forth/interp.rs | |
| parent | bd33129b72372512a4b8a570c101da01e8877fff (diff) | |
| download | automathon-c4b52f5f0770d3a83d67815b18e3cd76b01f0258.tar.gz automathon-c4b52f5f0770d3a83d67815b18e3cd76b01f0258.zip | |
add tail call elimination for call and if.
Diffstat (limited to 'src/forth/interp.rs')
| -rw-r--r-- | src/forth/interp.rs | 273 |
1 files changed, 268 insertions, 5 deletions
diff --git a/src/forth/interp.rs b/src/forth/interp.rs index 406c424..c7e8588 100644 --- a/src/forth/interp.rs +++ b/src/forth/interp.rs @@ -6,8 +6,14 @@ pub enum OpCode { Num(i32), Str(usize, usize), Call(usize), + TCall(usize), // tail call, really just ‘jmp’, but named to indicate desired usage. + If(usize, Option<usize>), // true word index, optional false word index. + TIf(usize, Option<usize>), // tail version Add, Sub, + // todo: maybe get rid of ret? it's only used in word definitions, + // which are bounded, so running of the end can be treated as an + // implicit ‘ret’. Ret, } @@ -28,7 +34,6 @@ impl Index<usize> for ByteCode { } } -// .0 is wordlist entry's bytecode, .1 is index into that bytecode #[derive(Debug, Copy, Clone, PartialEq)] pub(super) struct InstructionPointer { pub(super) word: usize, @@ -79,8 +84,19 @@ impl Interp { } } + pub fn jmp(&mut self, word: usize) { + self.ip.word = word; + self.ip.offset = 0; + } + + pub fn call(&mut self, word: usize) { + self.callstack.0.push(self.ip); + self.jmp(word); + } + pub fn tick(&mut self) -> Result<(), RuntimeError> { let bc = &self.wordlist.0[self.ip.word]; + eprintln!("debug: executing {:?}", bc[self.ip.offset]); match bc[self.ip.offset] { OpCode::Num(n) => self.stack.0.push(n), OpCode::Str(start, end) => eprintln!("got str: {} to {}", start, end), @@ -98,13 +114,45 @@ impl Interp { self.ip = self.callstack.0.pop().ok_or(RuntimeError::StackUnderflow)?; }, OpCode::Call(i) => { + eprintln!("should call word based on index {}", i); + self.call(i); + // skip the offset increment + return Ok(()) + }, + OpCode::TCall(i) => { eprintln!("should jump to word based on index {}", i); - self.callstack.0.push(self.ip); - self.ip.word = i; - self.ip.offset = 0; + self.jmp(i); // skip the offset increment return Ok(()) - } + }, + OpCode::If(true_clause, false_clause) => { + let flag = self.stack.0.pop().ok_or(RuntimeError::StackUnderflow)?; + if flag != 0 { + eprintln!("should call true branch {}", true_clause); + self.call(true_clause); + return Ok(()) + } else if let Some(false_clause) = false_clause { + eprintln!("should call false branch {}", false_clause); + self.call(false_clause); + return Ok(()) + } else { + eprintln!("should false fall through"); + } + }, + OpCode::TIf(true_clause, false_clause) => { + let flag = self.stack.0.pop().ok_or(RuntimeError::StackUnderflow)?; + if flag != 0 { + eprintln!("should call true branch {}", true_clause); + self.jmp(true_clause); + return Ok(()) + } else if let Some(false_clause) = false_clause { + eprintln!("should call false branch {}", false_clause); + self.jmp(false_clause); + return Ok(()) + } else { + eprintln!("should false fall through"); + } + }, } self.ip.offset += 1; Ok(()) @@ -193,4 +241,219 @@ mod tests { Ok(()) } + + #[test] + fn tail_call() -> Result<(), RuntimeError> { + let mut interp = Interp::new(); + interp.wordlist.0.push(ByteCode(vec![OpCode::Call(1)])); + interp.wordlist.0.push(ByteCode(vec![OpCode::TCall(2), OpCode::Ret])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Ret])); + + + interp.tick()?; // call + assert_eq!(interp.callstack.0.len(), 1, "callstack after call"); + interp.tick()?; // tcall + assert_eq!(interp.callstack.0.len(), 1, "callstack after tcall"); + interp.tick()?; // ret + assert_eq!(interp.callstack.0.len(), 0, "callstack after ret"); + Ok(()) + } + + #[test] + fn if_then_true() -> Result<(), RuntimeError> { + let mut interp = Interp::new(); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(-1), OpCode::If(1, None), OpCode::Num(0)])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(1), OpCode::Ret])); + + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push -1 stack len"); + assert_eq!(interp.stack.0[0], -1, "push -1 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "if stack "); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 1 stack len"); + assert_eq!(interp.stack.0[0], 1, "push 1 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "ret stack len"); + assert_eq!(interp.stack.0[0], 1, "ret stack val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 2, "push 0 stack len"); + assert_eq!(interp.stack.0[1], 0, "push 0 stack val"); + + Ok(()) + } + + #[test] + fn if_then_false() -> Result<(), RuntimeError> { + let mut interp = Interp::new(); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(0), OpCode::If(1, None), OpCode::Num(-1)])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(1), OpCode::Ret])); + + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 0 stack len"); + assert_eq!(interp.stack.0[0], 0, "push 0 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "if stack len"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push -1 stack len"); + assert_eq!(interp.stack.0[0], -1, "push -1 val"); + + Ok(()) + } + + #[test] + fn if_else_then_true() -> Result<(), RuntimeError> { + let mut interp = Interp::new(); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(-1), OpCode::If(1, Some(2)), OpCode::Num(-2)])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(1), OpCode::Ret])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(2), OpCode::Ret])); + + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push -1 stack len"); + assert_eq!(interp.stack.0[0], -1, "push -1 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "if stack len"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 1 stack len"); + assert_eq!(interp.stack.0[0], 1, "push 1 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "ret stack len"); + assert_eq!(interp.stack.0[0], 1, "ret val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 2, "push -2 stack len"); + assert_eq!(interp.stack.0[1], -2, "push -2 val"); + + Ok(()) + } + + #[test] + fn if_else_then_false() -> Result<(), RuntimeError> { + let mut interp = Interp::new(); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(0), OpCode::If(1, Some(2)), OpCode::Num(-2)])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(1), OpCode::Ret])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(2), OpCode::Ret])); + + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 0 stack len"); + assert_eq!(interp.stack.0[0], 0, "push 0 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "if"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 2 stack len"); + assert_eq!(interp.stack.0[0], 2, "push 2 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "ret"); + assert_eq!(interp.stack.0[0], 2, "ret"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 2, "push -2 stack len"); + assert_eq!(interp.stack.0[1], -2, "push -2 val"); + + Ok(()) + } + + #[test] + fn tail_if_then_true() -> Result<(), RuntimeError> { + let mut interp = Interp::new(); + interp.wordlist.0.push(ByteCode(vec![OpCode::Call(1), OpCode::Num(0)])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(-1), OpCode::TIf(2, None), OpCode::Ret])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(1), OpCode::Ret])); + + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "call(1) stack len"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push -1 stack len"); + assert_eq!(interp.stack.0[0], -1, "push -1 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "tif stack "); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 1 stack len"); + assert_eq!(interp.stack.0[0], 1, "push 1 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "ret stack len"); + assert_eq!(interp.stack.0[0], 1, "ret stack val"); + interp.tick()?; // no ret on wordlist[1] since it's ‘tif’ + assert_eq!(interp.stack.0.len(), 2, "push 0 stack len"); + assert_eq!(interp.stack.0[1], 0, "push 0 stack val"); + + Ok(()) + } + + #[test] + fn tail_if_then_false() -> Result<(), RuntimeError> { + let mut interp = Interp::new(); + interp.wordlist.0.push(ByteCode(vec![OpCode::Call(1), OpCode::Num(0)])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(0), OpCode::TIf(2, None), OpCode::Ret])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(1), OpCode::Ret])); + + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "call(1) stack len"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 0 stack len"); + assert_eq!(interp.stack.0[0], 0, "push 0 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "tif stack len"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "ret stack len"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 0 stack len"); + assert_eq!(interp.stack.0[0], 0, "push 0 val"); + + Ok(()) + } + + #[test] + fn tail_if_else_then_true() -> Result<(), RuntimeError> { + let mut interp = Interp::new(); + interp.wordlist.0.push(ByteCode(vec![OpCode::Call(1), OpCode::Num(-2)])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(-1), OpCode::TIf(2, Some(3)), OpCode::Ret])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(1), OpCode::Ret])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(2), OpCode::Ret])); + + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "call(1) stack len"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push -1 stack len"); + assert_eq!(interp.stack.0[0], -1, "push -1 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "tif stack len"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 1 stack len"); + assert_eq!(interp.stack.0[0], 1, "push 1 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "ret stack len"); + assert_eq!(interp.stack.0[0], 1, "ret val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 2, "push -2 stack len"); + assert_eq!(interp.stack.0[1], -2, "push -2 val"); + + Ok(()) + } + + #[test] + fn tail_if_else_then_false() -> Result<(), RuntimeError> { + let mut interp = Interp::new(); + interp.wordlist.0.push(ByteCode(vec![OpCode::Call(1), OpCode::Num(-2)])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(0), OpCode::TIf(2, Some(3)), OpCode::Ret])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(1), OpCode::Ret])); + interp.wordlist.0.push(ByteCode(vec![OpCode::Num(2), OpCode::Ret])); + + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "call(1) stack len"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 0 stack len"); + assert_eq!(interp.stack.0[0], 0, "push 0 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 0, "tif stack len"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "push 2 stack len"); + assert_eq!(interp.stack.0[0], 2, "push 2 val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 1, "ret stack len"); + assert_eq!(interp.stack.0[0], 2, "ret val"); + interp.tick()?; + assert_eq!(interp.stack.0.len(), 2, "push -2 stack len"); + assert_eq!(interp.stack.0[1], -2, "push -2 val"); + + Ok(()) + } } |
