get binding & pretty debugging working
This commit is contained in:
parent
48754f92a4
commit
d4342b0623
|
@ -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`
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
15
src/value.rs
15
src/value.rs
|
@ -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::*;
|
||||||
|
|
32
src/vm.rs
32
src/vm.rs
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user