Implement augmented assignment operators

Add support for +=, -=, *=, and /= operators. This is basically just
syntactic sugar, but it's still nice to have

    a += 1

compiles to the equivalent of

    a = a + 1

with all the same implications of scoping rules.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2024-10-07 10:23:15 -07:00
parent 8179611c23
commit 365bee0554
5 changed files with 111 additions and 36 deletions

View File

@@ -417,6 +417,29 @@ impl<'c> Compiler<'c> {
Ok(())
}
/// Emit a name lookup. This will either emit `Op::GetLocal` or `Op::GetGlobal`, depending on
/// the context. If the local or global is not found in the current scope, we error out
/// instead.
fn emit_lookup(&mut self, line: LineRange, name: &str) -> Result<()> {
// check if there's a local with this name, otherwise check globals
if let Some(local) = self.get_local(name) {
self.emit(line, Op::GetLocal(local.index));
} else {
let global = self.get_global(name).ok_or_else(|| CompileError::Error {
line: Some(line),
source_path: self.path.display().to_string(),
message: if self.is_global_scope() {
format!("unknown global {}", name)
} else {
format!("unknown local {}", name)
},
})?;
self.emit(line, Op::GetGlobal(global));
}
Ok(())
}
fn search_dir(&self) -> &Path {
self.path.parent().unwrap()
}
@@ -524,23 +547,52 @@ impl StmtVisitor for Compiler<'_> {
}
fn visit_assign_stmt(&mut self, stmt: &AssignStmt) -> Result<()> {
// compile RHS
self.compile_expr(&stmt.rhs)?;
let name = &stmt.lhs.text;
// If the last value that was assigned to is a function, set its name here
// TODO - maybe this would be smarter to set up in the AST. I'm 99% sure that the last
// object created, if it were a function object, will be what we're assigning it to, but I
// want to be 100% sure instead of 99%.
if let Some(obj) = self.constants.last() {
if let Some(fun) = obj.borrow_mut().as_any_mut().downcast_mut::<UserFunction>() {
fun.set_name(Rc::new(name.to_string()));
}
}
let line = stmt_line_number(stmt);
// compile LHS
self.emit_assign(stmt_line_number(stmt), name)?;
match stmt.op.kind {
// normal assignment
TokenKind::Eq => {
// compile RHS
self.compile_expr(&stmt.rhs)?;
// If the last value that was assigned to is a function, set its name here
// TODO - maybe this would be smarter to set up in the AST. I'm 99% sure that the last
// object created, if it were a function object, will be what we're assigning it to, but I
// want to be 100% sure instead of 99%.
if let Some(obj) = self.constants.last() {
if let Some(fun) = obj.borrow_mut().as_any_mut().downcast_mut::<UserFunction>()
{
fun.set_name(Rc::new(name.to_string()));
}
}
// compile LHS
self.emit_assign(line, name)?;
}
// augmented assignment
TokenKind::PlusEq | TokenKind::MinusEq | TokenKind::StarEq | TokenKind::SlashEq => {
static OP_NAMES: LazyLock<HashMap<TokenKind, &'static str>> = LazyLock::new(|| {
hash_map! {
TokenKind::PlusEq => "__add__",
TokenKind::MinusEq => "__sub__",
TokenKind::StarEq => "__mul__",
TokenKind::SlashEq => "__div__",
}
});
self.emit_lookup(line, name)?;
let op_name = OP_NAMES
.get(&stmt.op.kind)
.expect("invalid augmented assignment operator");
let op_constant = self.insert_constant(Str::create(op_name))?;
self.emit(line, Op::GetAttr(op_constant));
self.compile_expr(&stmt.rhs)?;
self.emit(line, Op::Call(1));
self.emit_assign(line, name)?;
}
_ => unreachable!(),
}
Ok(())
}
@@ -750,21 +802,8 @@ impl ExprVisitor for Compiler<'_> {
match expr.token.kind {
TokenKind::Name => {
let name = &expr.token.text;
// check if there's a local with this name, otherwise check globals
if let Some(local) = self.get_local(name) {
self.emit(expr_line_number(expr), Op::GetLocal(local.index));
} else {
let global = self.get_global(name).ok_or_else(|| CompileError::Error {
line: Some(expr_line_number(expr)),
source_path: self.path.display().to_string(),
message: if self.is_global_scope() {
format!("unknown global {}", name)
} else {
format!("unknown local {}", name)
},
})?;
self.emit(expr_line_number(expr), Op::GetGlobal(global));
}
let line = expr_line_number(expr);
self.emit_lookup(line, name)?;
}
TokenKind::Number => {
let obj = if expr.token.text.contains('.') {