//! implements SideEffectChecker //! SideEffectCheckerを実装 //! 関数や不変型に副作用がないかチェックする use erg_common::log; use erg_common::traits::Stream; use erg_common::vis::Visibility; use erg_common::Str; use Visibility::*; use crate::error::{EffectError, EffectErrors, EffectResult}; use crate::hir::{Accessor, Array, Def, Expr, Signature, Tuple, HIR}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum BlockKind { // forbid side effects Func, ConstFunc, ConstInstant, // e.g. Type definition // allow side effects Proc, Instant, Module, } use BlockKind::*; #[derive(Debug)] pub struct SideEffectChecker { path_stack: Vec<(Str, Visibility)>, block_stack: Vec, errs: EffectErrors, } impl SideEffectChecker { pub fn new() -> Self { Self { path_stack: vec![], block_stack: vec![], errs: EffectErrors::empty(), } } fn full_path(&self) -> String { self.path_stack .iter() .fold(String::new(), |acc, (path, vis)| { if vis.is_public() { acc + "." + &path[..] } else { acc + "::" + &path[..] } }) } /// It is permitted to define a procedure in a function, /// and of course it is permitted to cause side effects in a procedure /// /// However, it is not permitted to cause side effects within an instant block in a function /// (side effects are allowed in instant blocks in procedures and modules) fn in_context_effects_allowed(&self) -> bool { // if toplevel if self.block_stack.len() == 1 { return true; } match ( self.block_stack.get(self.block_stack.len() - 2).unwrap(), self.block_stack.last().unwrap(), ) { (_, Func | ConstInstant) => false, (_, Proc) => true, (Proc | Module | Instant, Instant) => true, _ => false, } } pub fn check(mut self, hir: HIR) -> EffectResult { self.path_stack.push((hir.name.clone(), Private)); self.block_stack.push(Module); log!(info "the side-effects checking process has started.{RESET}"); // トップレベルでは副作用があっても問題なく、純粋性違反がないかのみチェックする for expr in hir.module.iter() { match expr { Expr::Def(def) => { self.check_def(def); } Expr::ClassDef(class_def) => { // TODO: grow for def in class_def.methods.iter() { self.check_expr(def); } } Expr::Call(call) => { for parg in call.args.pos_args.iter() { self.check_expr(&parg.expr); } for kwarg in call.args.kw_args.iter() { self.check_expr(&kwarg.expr); } } Expr::BinOp(bin) => { self.check_expr(&bin.lhs); self.check_expr(&bin.rhs); } Expr::UnaryOp(unary) => { self.check_expr(&unary.expr); } Expr::Accessor(_) | Expr::Lit(_) => {} Expr::Array(array) => match array { Array::Normal(arr) => { for elem in arr.elems.pos_args.iter() { self.check_expr(&elem.expr); } } Array::WithLength(arr) => { self.check_expr(&arr.elem); self.check_expr(&arr.len); } Array::Comprehension(arr) => { self.check_expr(&arr.elem); self.check_expr(&arr.guard); } }, Expr::Tuple(tuple) => match tuple { Tuple::Normal(tuple) => { for elem in tuple.elems.pos_args.iter() { self.check_expr(&elem.expr); } } }, Expr::Record(rec) => { self.path_stack.push((Str::ever(""), Private)); self.block_stack.push(Instant); for attr in rec.attrs.iter() { self.check_def(attr); } self.path_stack.pop(); self.block_stack.pop(); } Expr::TypeAsc(tasc) => { self.check_expr(&tasc.expr); } other => todo!("{other}"), } } log!(info "the side-effects checking process has completed, found errors: {}{RESET}", self.errs.len()); if self.errs.is_empty() { Ok(hir) } else { Err(self.errs) } } fn check_def(&mut self, def: &Def) { let name_and_vis = match &def.sig { Signature::Var(var) => (var.inspect().clone(), var.vis()), Signature::Subr(subr) => (subr.ident.inspect().clone(), subr.ident.vis()), }; self.path_stack.push(name_and_vis); // TODO: support raw identifier (``) let is_procedural = def.sig.is_procedural(); let is_subr = def.sig.is_subr(); let is_const = def.sig.is_const(); match (is_procedural, is_subr, is_const) { (true, true, true) => { panic!("user-defined constant procedures are not allowed"); } (true, true, false) => { self.block_stack.push(Proc); } (_, false, false) => { self.block_stack.push(Instant); } (false, true, true) => { self.block_stack.push(ConstFunc); } (false, true, false) => { self.block_stack.push(Func); } (_, false, true) => { self.block_stack.push(ConstInstant); } } for chunk in def.body.block.iter() { self.check_expr(chunk); } self.path_stack.pop(); self.block_stack.pop(); } /// check if `expr` has side-effects / purity violations. /// /// returns effects, purity violations will be appended to `self.errs`. /// /// causes side-effects: /// ```python /// p!() // 1 side-effect /// p!(q!()) // 2 side-effects /// x = /// y = r!() /// y + 1 // 1 side-effect /// ``` /// causes no side-effects: /// ```python /// q! = p! /// y = f(p!) /// ``` /// purity violation: /// ```python /// for iter, i -> print! i /// ``` fn check_expr(&mut self, expr: &Expr) { match expr { Expr::Def(def) => { self.check_def(def); } Expr::ClassDef(class_def) => { for def in class_def.methods.iter() { self.check_expr(def); } } Expr::Array(array) => match array { Array::Normal(arr) => { for elem in arr.elems.pos_args.iter() { self.check_expr(&elem.expr); } } Array::WithLength(arr) => { self.check_expr(&arr.elem); self.check_expr(&arr.len); } Array::Comprehension(arr) => { self.check_expr(&arr.elem); self.check_expr(&arr.guard); } }, Expr::Tuple(tuple) => match tuple { Tuple::Normal(tup) => { for arg in tup.elems.pos_args.iter() { self.check_expr(&arg.expr); } } }, Expr::Record(record) => { self.path_stack.push((Str::ever(""), Private)); self.block_stack.push(Instant); for attr in record.attrs.iter() { self.check_def(attr); } self.path_stack.pop(); self.block_stack.pop(); } // 引数がproceduralでも関数呼び出しなら副作用なし Expr::Call(call) => { if (self.is_procedural(&call.obj) || call .method_name .as_ref() .map(|name| name.is_procedural()) .unwrap_or(false)) && !self.in_context_effects_allowed() { self.errs.push(EffectError::has_effect( line!() as usize, expr, self.full_path(), )); } call.args .pos_args .iter() .for_each(|parg| self.check_expr(&parg.expr)); call.args .kw_args .iter() .for_each(|kwarg| self.check_expr(&kwarg.expr)); } Expr::UnaryOp(unary) => { self.check_expr(&unary.expr); } Expr::BinOp(bin) => { self.check_expr(&bin.lhs); self.check_expr(&bin.rhs); } Expr::Lambda(lambda) => { let is_proc = lambda.is_procedural(); if is_proc { self.path_stack.push((Str::ever(""), Private)); self.block_stack.push(Proc); } else { self.path_stack.push((Str::ever(""), Private)); self.block_stack.push(Func); } lambda.body.iter().for_each(|chunk| self.check_expr(chunk)); self.path_stack.pop(); self.block_stack.pop(); } Expr::TypeAsc(type_asc) => { self.check_expr(&type_asc.expr); } _ => {} } } fn is_procedural(&self, expr: &Expr) -> bool { match expr { Expr::Lambda(lambda) => lambda.is_procedural(), // 引数がproceduralでも関数呼び出しなら副作用なし Expr::Call(call) => self.is_procedural(&call.obj), Expr::Accessor(Accessor::Ident(ident)) => ident.name.is_procedural(), // procedural: x.y! (e.g. Array.sample!) // !procedural: !x.y Expr::Accessor(Accessor::Attr(attr)) => attr.ident.is_procedural(), Expr::Accessor(_) => todo!(), Expr::TypeAsc(tasc) => self.is_procedural(&tasc.expr), _ => false, } } } impl Default for SideEffectChecker { fn default() -> Self { Self::new() } }