get binding & pretty debugging working

This commit is contained in:
Scott Richmond 2024-12-18 01:28:23 -05:00
parent 48754f92a4
commit d4342b0623
6 changed files with 143 additions and 25 deletions

View File

@ -84,4 +84,50 @@ let number = ((first as u16) << 8) | second as u16;
#### Oy, stacks and expressions #### Oy, stacks and expressions
One thing that's giving me grief is when to pop and when to note on the value stack. One thing that's giving me grief is when to pop and when to note on the value stack.
Consider So, like, we need to make sure that a line of code leaves the stack exactly where it was before it ran, with the exception of binding forms: `let`, `fn`, `box`, etc. Those leave one (or more!) items on the stack.
In the simplest case, we have a line of code that's just a constant:
```
false
```
This should emit the bytecode instructions (more or less):
```
push false
pop
```
The push comes from the `false` value.
The pop comes from the end of a (nonbinding) line.
The problem is that there's no way (at all, in Ludus) to distinguish between an expression that's just a constant and a line that is a complete line of code that's an expression.
So if we have the following:
```
let foo = false
```
We want:
```
push false
```
Or, rather, given that `foo` is a word pattern, what we actually want is:
```
push false # constant
push pattern/word # load pattern
pop
pop # compare
push false # for the binding
```
But it's worth it here to explore Ludus's semantics.
It's the case that there are actually only three binding forms (for now): `let`, `fn`, and `box`.
Figuring out `let` will help a great deal.
Match also binds things, but at the very least, match doesn't bind with expressions on the rhs, but a single value.
Think, too about expressions: everything comes down to a single value (of course), even tuples (especially now that I'm separating function calls from tuple values (probably)).
So: anything that *isn't* a binding form should, before the `pop` from the end of a line, only leave a single value on the stack.
Which suggests that, as odd as it is, pushing a single `nil` onto the stack, just to pop it, might make sense.
Or, perhaps the thing to do is to peek: if the line in question is binding or not, then emit different bytecode.
That's probably the thing to do. Jesus, Scott.
And **another** thing worth internalizing: every single instruction that's not an explicit push or pop should leave the stack length unchanged.
So store and load need always to swap in a `nil`

View File

@ -54,6 +54,16 @@ pub struct Chunk<'a> {
pub name: &'static str, pub name: &'static str,
} }
fn is_binding(expr: &Spanned<Ast>) -> bool {
let (ast, _) = expr;
use Ast::*;
match ast {
Let(..) | LBox(..) => true,
Fn(name, ..) => *name != "*anon",
_ => false,
}
}
impl<'a> Chunk<'a> { impl<'a> Chunk<'a> {
pub fn new(ast: &'a Spanned<Ast>, name: &'static str, src: &'static str) -> Chunk<'a> { pub fn new(ast: &'a Spanned<Ast>, name: &'static str, src: &'static str) -> Chunk<'a> {
Chunk { Chunk {
@ -114,12 +124,10 @@ impl<'a> Chunk<'a> {
} }
fn bind(&mut self, name: &'static str) { fn bind(&mut self, name: &'static str) {
println!("binding {name} at depth {}", self.scope_depth);
self.bindings.push(Binding { self.bindings.push(Binding {
name, name,
depth: self.scope_depth, depth: self.scope_depth,
}); });
println!("{:?}", self.bindings)
} }
pub fn compile(&mut self) { pub fn compile(&mut self) {
@ -144,11 +152,15 @@ impl<'a> Chunk<'a> {
} }
Block(lines) => { Block(lines) => {
self.scope_depth += 1; self.scope_depth += 1;
println!("now entering scope level {}", self.scope_depth); for expr in lines.iter().take(lines.len() - 1) {
for expr in lines { if is_binding(expr) {
self.visit(expr); self.visit(expr)
self.emit_op(Op::Pop); } else {
self.visit(expr);
self.emit_op(Op::Pop);
}
} }
self.visit(lines.last().unwrap());
self.emit_op(Op::Store); self.emit_op(Op::Store);
self.scope_depth -= 1; self.scope_depth -= 1;
while let Some(binding) = self.bindings.last() { while let Some(binding) = self.bindings.last() {
@ -178,7 +190,6 @@ impl<'a> Chunk<'a> {
self.bytecode[jump_idx + 1] = jump_offset as u8; self.bytecode[jump_idx + 1] = jump_offset as u8;
} }
Let(patt, expr) => { Let(patt, expr) => {
println!("let binding!");
self.visit(expr); self.visit(expr);
self.visit(patt); self.visit(patt);
} }
@ -186,12 +197,9 @@ impl<'a> Chunk<'a> {
self.bind(name); self.bind(name);
} }
Word(name) => { Word(name) => {
println!("resolving binding {name}");
println!("current bindings {:?}", self.bindings);
self.emit_op(Op::PushBinding); self.emit_op(Op::PushBinding);
let biter = self.bindings.iter().enumerate().rev(); let biter = self.bindings.iter().enumerate().rev();
for (i, binding) in biter { for (i, binding) in biter {
println!("at index {i}");
if binding.name == *name { if binding.name == *name {
self.bytecode.push(i as u8); self.bytecode.push(i as u8);
break; break;
@ -230,4 +238,25 @@ impl<'a> Chunk<'a> {
} }
} }
} }
pub fn dissasemble_instr(&self, i: usize) {
let op = Op::from_u8(self.bytecode[i]).unwrap();
use Op::*;
match op {
Pop | Store | Load => println!("{i:04}: {op}"),
Constant => {
let next = self.bytecode[i + 1];
let value = &self.constants[next as usize].show(self);
println!("{i:04}: {:16} {next:04}: {value}", op.to_string());
}
PushBinding => {
let next = self.bytecode[i + 1];
println!("{i:04}: {:16} {next:04}", op.to_string());
}
Jump | JumpIfFalse => {
let next = self.bytecode[i + 1];
println!("{i:04}: {:16} {next:04}", op.to_string())
}
}
}
} }

View File

@ -1,5 +1,8 @@
use chumsky::{input::Stream, prelude::*}; use chumsky::{input::Stream, prelude::*};
const DEBUG_COMPILE: bool = true;
const DEBUG_RUN: bool = true;
mod memory_sandbox; mod memory_sandbox;
mod spans; mod spans;
@ -41,7 +44,14 @@ pub fn run(src: &'static str) {
let mut chunk = Chunk::new(&parsed, "test", src); let mut chunk = Chunk::new(&parsed, "test", src);
chunk.compile(); chunk.compile();
chunk.disassemble(); if DEBUG_COMPILE {
chunk.disassemble();
println!("\n\n")
}
if DEBUG_RUN {
println!("=== vm run: test ===");
}
let mut vm = Vm::new(&chunk); let mut vm = Vm::new(&chunk);
let result = vm.interpret(); let result = vm.interpret();
@ -58,15 +68,9 @@ let foo = :let_foo
let bar = if true let bar = if true
then { then {
:foo let baz = :baz
:bar
:baz
}
else {
1
2
3
} }
else :whatever
foo foo

View File

@ -922,7 +922,7 @@ where
let lambda = just(Token::Reserved("fn")) let lambda = just(Token::Reserved("fn"))
.ignore_then(fn_unguarded.clone()) .ignore_then(fn_unguarded.clone())
.map_with(|clause, e| (Fn("anonymous", vec![clause], None), e.span())); .map_with(|clause, e| (Fn("*anon", vec![clause], None), e.span()));
let fn_clauses = fn_clause let fn_clauses = fn_clause
.clone() .clone()

View File

@ -41,6 +41,21 @@ pub enum Value {
Fn(Rc<RefCell<LFn>>), Fn(Rc<RefCell<LFn>>),
} }
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use Value::*;
match self {
Nil => write!(f, "nil"),
True => write!(f, "true"),
False => write!(f, "false"),
Keyword(idx) => write!(f, ":{idx}"),
Interned(idx) => write!(f, "\"@{idx}\""),
Number(n) => write!(f, "{n}"),
_ => todo!(),
}
}
}
impl Value { impl Value {
pub fn show(&self, ctx: &Chunk) -> String { pub fn show(&self, ctx: &Chunk) -> String {
use Value::*; use Value::*;

View File

@ -44,20 +44,35 @@ impl<'a> Vm<'a> {
} }
pub fn push(&mut self, value: Value) { pub fn push(&mut self, value: Value) {
println!("{:04} pushing {value:?}", self.ip);
self.stack.push(value); self.stack.push(value);
} }
pub fn pop(&mut self) -> Value { pub fn pop(&mut self) -> Value {
let value = self.stack.pop().unwrap(); self.stack.pop().unwrap()
println!("{:04} popping {value:?}", self.ip); }
value
fn print_stack(&self) {
let inner = self
.stack
.iter()
.map(|val| val.to_string())
.collect::<Vec<_>>()
.join("|");
println!("{:04}: [{inner}] {}", self.ip, self.return_register);
}
fn print_debug(&self) {
self.chunk.dissasemble_instr(self.ip);
self.print_stack();
} }
pub fn interpret(&mut self) -> Result<Value, Panic> { pub fn interpret(&mut self) -> Result<Value, Panic> {
let Some(byte) = self.chunk.bytecode.get(self.ip) else { let Some(byte) = self.chunk.bytecode.get(self.ip) else {
return Ok(self.stack.pop().unwrap()); return Ok(self.stack.pop().unwrap());
}; };
if crate::DEBUG_RUN {
self.print_debug();
}
let op = Op::from_u8(*byte).unwrap(); let op = Op::from_u8(*byte).unwrap();
use Op::*; use Op::*;
match op { match op {
@ -107,7 +122,16 @@ impl<'a> Vm<'a> {
} }
Load => { Load => {
let mut value = Value::Nil; let mut value = Value::Nil;
// println!(
// "before swap, return register holds: {}",
// self.return_register
// );
swap(&mut self.return_register, &mut value); swap(&mut self.return_register, &mut value);
// println!(
// "before swap, return register holds: {}",
// self.return_register
// );
// println!("now local value holds {value}");
self.push(value); self.push(value);
self.ip += 1; self.ip += 1;
self.interpret() self.interpret()