add list patterns, fix dict stack mechanics

This commit is contained in:
Scott Richmond 2025-05-25 16:30:20 -04:00
parent 4f7ba56d1f
commit cf51822128
3 changed files with 85 additions and 45 deletions

View File

@ -1,5 +1,4 @@
use crate::parser::Ast;
use crate::pattern::Pattern;
use crate::spans::Spanned;
use crate::value::*;
use chumsky::prelude::SimpleSpan;
@ -32,6 +31,8 @@ pub enum Op {
MatchTuple,
PushTuple,
LoadTuple,
MatchList,
LoadList,
PushList,
PushDict,
PushBox,
@ -75,6 +76,8 @@ impl std::fmt::Display for Op {
MatchTuple => "match_tuple",
PushTuple => "push_tuple",
LoadTuple => "load_tuple",
MatchList => "match_list",
LoadList => "load_list",
PushList => "push_list",
PushDict => "push_dict",
PushBox => "push_box",
@ -117,7 +120,7 @@ impl Chunk {
match op {
Pop | Store | Load | Nil | True | False | MatchNil | MatchTrue | MatchFalse
| PanicIfNoMatch | MatchWord | ResetMatch | GetKey | PanicNoWhen | PanicNoMatch
| TypeOf | Duplicate | Decrement | Truncate | Noop | LoadTuple => {
| TypeOf | Duplicate | Decrement | Truncate | Noop | LoadTuple | LoadList => {
println!("{i:04}: {op}")
}
Constant | MatchConstant => {
@ -125,9 +128,9 @@ impl Chunk {
let value = &self.constants[next as usize].show(self);
println!("{i:04}: {:16} {next:04}: {value}", op.to_string());
}
PushBinding | MatchTuple | PushTuple | PushDict | PushList | PushBox | Jump
| JumpIfFalse | JumpIfNoMatch | JumpIfMatch | JumpBack | JumpIfZero | MatchDepth
| PopN => {
PushBinding | MatchTuple | MatchList | PushTuple | PushDict | PushList | PushBox
| Jump | JumpIfFalse | JumpIfNoMatch | JumpIfMatch | JumpBack | JumpIfZero
| MatchDepth | PopN => {
let next = self.bytecode[i + 1];
println!("{i:04}: {:16} {next:04}", op.to_string());
}
@ -260,7 +263,6 @@ impl Compiler {
self.spans.push(self.span);
self.chunk.bytecode.push(constant_index as u8);
self.spans.push(self.span);
// self.bind("");
}
fn emit_op(&mut self, op: Op) {
@ -434,19 +436,15 @@ impl Compiler {
}
PlaceholderPattern => {
self.emit_op(Op::MatchWord);
// self.bind("");
}
NilPattern => {
self.emit_op(Op::MatchNil);
// self.bind("");
}
BooleanPattern(b) => {
if *b {
self.emit_op(Op::MatchTrue);
// self.bind("");
} else {
self.emit_op(Op::MatchFalse);
// self.bind("");
}
}
NumberPattern(n) => {
@ -474,28 +472,6 @@ impl Compiler {
}
self.match_constant(Value::Interned(str_index));
}
// TODO: finish this work
// What's going on:
// Currently, bindings are made in lockstep with the stack.
// And the index of the binding in the bindings array in the compiler gets converted to a u8 as the index into the stack
// I suspect this will have to change when we get stack frames
// But what's happening here is that nested tuple bindings are out of order
// When a tuple gets unfolded at the end of the stack, the binding that matches is now not where you'd expect
// The whole "match depth" construct, while working, is what allows for out-of-order matching/binding
// So either:
// - Bindings need to work differently, where there's some way of representing them that's not just the index of where the name is
// - Or we need a way of working with nested tuples that ensure bindings continue to be orederly
// My sense is that the former is the correct approach.
// It introduces some complexity: a binding will have to be a `(name, offset)` tuple or some such
// Working with nested tuples could perhaps be solved by representing tuples on the stack, but then you're going to run into similar problems with list and dict patterns
// And so, the thing to, I think, is to get clever with an offset
// But to do that, probably the compiler needs to model the stack? Ugh.
// SO, THEN:
// [x] 1. we need a stack counter, that increases any time anything gets pushed to the stack, and decreases any time anything gets popped
// [x] 2. We need to change the representation of bindings to be a tuple (name, index), where index is `stack size - match depth`
// [x] 3. This means we get to remove the "silent" bindings where all patterns add a `""` binding
// [x] 4. Question: given that we need both of these things, should I model this as methods rather than explicit `emit_op` calls? Probably.
// Currently: there's still an off by one error with the current test code with nested tuples, but I can prolly fix this?
TuplePattern(members) => {
self.emit_op(Op::MatchTuple);
self.emit_byte(members.len());
@ -522,10 +498,10 @@ impl Compiler {
self.chunk.bytecode[idx] = self.len() as u8 - idx as u8 - 1;
}
for _ in 0..members.len() {
// self.pop();
// this only runs if there's no match
// so don't change the representation of the stack
// contingencies will be handled by the binding forms
// (i.e., `let` and `match`)
// thus: emit Op::Pop directly
self.emit_op(Op::Pop);
}
@ -533,6 +509,43 @@ impl Compiler {
self.len() as u8 - before_load_tup_idx as u8 - 1;
self.chunk.bytecode[jump_idx] = self.len() as u8 - jump_idx as u8 - 1;
}
ListPattern(members) => {
self.emit_op(Op::MatchList);
self.emit_byte(members.len());
self.emit_op(Op::JumpIfNoMatch);
let before_load_list_idx = self.len();
self.emit_byte(0xff);
let mut jump_idxes = vec![];
self.match_depth += members.len();
self.emit_op(Op::LoadList);
self.stack_depth += members.len();
for member in members {
self.match_depth -= 1;
self.emit_op(Op::MatchDepth);
self.emit_byte(self.match_depth);
self.visit(member);
self.emit_op(Op::JumpIfNoMatch);
jump_idxes.push(self.len());
self.emit_byte(0xff);
}
self.emit_op(Op::Jump);
let jump_idx = self.len();
self.emit_byte(0xff);
for idx in jump_idxes {
self.chunk.bytecode[idx] = self.len() as u8 - idx as u8 - 1;
}
for _ in 0..members.len() {
// this only runs if there's no match
// so don't change the representation of the stack
// contingencies will be handled by the binding forms
// (i.e., `let` and `match`)
// thus: emit Op::Pop directly
self.emit_op(Op::Pop);
}
self.chunk.bytecode[before_load_list_idx] =
self.len() as u8 - before_load_list_idx as u8 - 1;
self.chunk.bytecode[jump_idx] = self.len() as u8 - jump_idx as u8 - 1;
}
Tuple(members) => {
for member in members {
self.visit(member);
@ -556,12 +569,20 @@ impl Compiler {
self.bind(name);
}
Dict(pairs) => {
println!("Compiling dict of len {}", pairs.len());
println!("Stack len: {}", self.stack_depth);
for pair in pairs {
self.visit(pair);
println!("Visited pair {:?}", pair);
println!("Current stack len: {}", self.stack_depth);
}
self.emit_op(Op::PushDict);
self.emit_byte(pairs.len());
self.stack_depth = self.stack_depth + 1 - pairs.len();
self.stack_depth = self.stack_depth + 1 - (pairs.len() * 2);
println!(
"Finished compiling dict, stack len now: {}",
self.stack_depth
);
}
Pair(key, value) => {
let existing_kw = self.chunk.keywords.iter().position(|kw| kw == key);
@ -597,10 +618,6 @@ impl Compiler {
todo!()
}
}
// TODO: Keep track of the stack in
// WHEN and MATCH:
// each needs to just hold onto the stack depth
// before each clause, and reset it after each
When(clauses) => {
let mut jump_idxes = vec![];
let mut clauses = clauses.iter();
@ -611,11 +628,11 @@ impl Compiler {
self.emit_byte(0xff);
self.stack_depth -= 1;
self.visit(body);
self.stack_depth -= 1;
self.emit_op(Op::Jump);
jump_idxes.push(self.len());
self.emit_byte(0xff);
self.chunk.bytecode[jif_jump_idx] = self.len() as u8 - jif_jump_idx as u8 - 1;
self.stack_depth -= 1;
}
self.emit_op(Op::PanicNoWhen);
for idx in jump_idxes {
@ -824,7 +841,6 @@ impl Compiler {
| InterpolatedPattern(..)
| AsPattern(..)
| Splattern(..)
| ListPattern(..)
| PairPattern(..)
| DictPattern(..) => todo!(),
}
@ -840,7 +856,8 @@ impl Compiler {
match op {
Noop | Pop | Store | Load | Nil | True | False | MatchNil | MatchTrue
| MatchFalse | MatchWord | ResetMatch | PanicIfNoMatch | GetKey | PanicNoWhen
| PanicNoMatch | TypeOf | Duplicate | Truncate | Decrement | LoadTuple => {
| PanicNoMatch | TypeOf | Duplicate | Truncate | Decrement | LoadTuple
| LoadList => {
println!("{i:04}: {op}")
}
Constant | MatchConstant => {
@ -848,9 +865,9 @@ impl Compiler {
let value = &self.chunk.constants[*next as usize].show(&self.chunk);
println!("{i:04}: {:16} {next:04}: {value}", op.to_string());
}
PushBinding | MatchTuple | PushTuple | PushDict | PushList | PushBox | Jump
| JumpIfFalse | JumpIfNoMatch | JumpIfMatch | JumpBack | JumpIfZero
| MatchDepth | PopN => {
PushBinding | MatchTuple | MatchList | PushTuple | PushDict | PushList
| PushBox | Jump | JumpIfFalse | JumpIfNoMatch | JumpIfMatch | JumpBack
| JumpIfZero | MatchDepth | PopN => {
let (_, next) = codes.next().unwrap();
println!("{i:04}: {:16} {next:04}", op.to_string());
}

View File

@ -74,7 +74,7 @@ pub fn run(src: &'static str) {
pub fn main() {
env::set_var("RUST_BACKTRACE", "1");
let src = "
#{:a 1, :b 2, :c 3}
";
run(src);
}

View File

@ -265,6 +265,29 @@ impl<'a> Vm<'a> {
self.stack.push(list);
self.ip += 2;
}
MatchList => {
let idx = self.stack.len() - self.match_depth as usize - 1;
let tuple_len = self.chunk.bytecode[self.ip + 1];
let scrutinee = self.stack[idx].clone();
match scrutinee {
Value::List(members) => self.matches = members.len() == tuple_len as usize,
_ => self.matches = false,
};
self.ip += 2;
}
LoadList => {
let idx = self.stack.len() - self.match_depth as usize - 1;
let tuple = self.stack[idx].clone();
match tuple {
Value::List(members) => {
for member in members.iter() {
self.push(member.clone());
}
}
_ => self.panic("internal error: expected tuple"),
};
self.ip += 1;
}
PushDict => {
let dict_len = self.chunk.bytecode[self.ip + 1] as usize * 2;
let dict_members = self.stack.split_off(self.stack.len() - dict_len);