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:
@@ -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('.') {
|
||||
|
||||
Reference in New Issue
Block a user