erg_compiler/
effectcheck.rs

1//! implements SideEffectChecker
2//! SideEffectCheckerを実装
3//! 関数や不変型に副作用がないかチェックする
4
5use erg_common::config::ErgConfig;
6use erg_common::log;
7use erg_common::traits::{Locational, Stream};
8use erg_common::Str;
9use erg_parser::token::TokenKind;
10
11use crate::context::Context;
12use crate::error::{EffectError, EffectErrors};
13use crate::hir::{Call, Def, Dict, Expr, List, Params, Set, Signature, Tuple, HIR};
14use crate::ty::{HasType, Visibility};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17enum BlockKind {
18    // forbid side effects
19    Func,
20    ConstFunc,
21    ConstInstant, // e.g. Type definition
22    // allow side effects
23    Proc,
24    Instant,
25    Module,
26}
27
28use BlockKind::*;
29
30/// Checks code for side effects.
31/// For example:
32/// * check if expressions with side effects are not used in functions
33/// * check if methods that change internal state are not defined in immutable classes
34#[derive(Debug)]
35pub struct SideEffectChecker<'c> {
36    cfg: ErgConfig,
37    path_stack: Vec<Visibility>,
38    block_stack: Vec<BlockKind>,
39    errs: EffectErrors,
40    ctx: &'c Context,
41}
42
43impl<'c> SideEffectChecker<'c> {
44    pub fn new(cfg: ErgConfig, ctx: &'c Context) -> Self {
45        Self {
46            cfg,
47            path_stack: vec![],
48            block_stack: vec![],
49            errs: EffectErrors::empty(),
50            ctx,
51        }
52    }
53
54    fn full_path(&self) -> String {
55        self.path_stack
56            .iter()
57            .fold(String::new(), |acc, vis| {
58                if vis.is_public() {
59                    acc + "." + &vis.def_namespace[..]
60                } else {
61                    acc + "::" + &vis.def_namespace[..]
62                }
63            })
64            .trim_start_matches(['.', ':'].as_ref())
65            .to_string()
66    }
67
68    /// It is permitted to define a procedure in a function,
69    /// and of course it is permitted to cause side effects in a procedure
70    ///
71    /// However, it is not permitted to cause side effects within an instant block in a function
72    /// (side effects are allowed in instant blocks in procedures and modules)
73    fn in_context_effects_allowed(&self) -> bool {
74        // if toplevel
75        if self.block_stack.len() == 1 {
76            return true;
77        }
78        match (
79            self.block_stack.get(self.block_stack.len() - 2).unwrap(),
80            self.block_stack.last().unwrap(),
81        ) {
82            (_, Func | ConstInstant) => false,
83            (_, Proc) => true,
84            (Proc | Module | Instant, Instant) => true,
85            _ => false,
86        }
87    }
88
89    pub fn check(mut self, hir: HIR, namespace: Str) -> Result<HIR, (HIR, EffectErrors)> {
90        self.path_stack.push(Visibility::private(namespace));
91        self.block_stack.push(Module);
92        log!(info "the side-effects checking process has started.{RESET}");
93        // At the top level, there is no problem with side effects, only check for purity violations.
94        // トップレベルでは副作用があっても問題なく、純粋性違反がないかのみチェックする
95        for expr in hir.module.iter() {
96            match expr {
97                Expr::Def(def) => {
98                    self.check_def(def);
99                }
100                Expr::ClassDef(class_def) => {
101                    if let Some(req_sup) = &class_def.require_or_sup {
102                        self.check_expr(req_sup);
103                    }
104                    let name_and_vis = Visibility::new(
105                        class_def.sig.vis().clone(),
106                        class_def.sig.inspect().clone(),
107                    );
108                    self.path_stack.push(name_and_vis);
109                    for def in class_def.all_methods() {
110                        self.check_expr(def);
111                    }
112                    self.path_stack.pop();
113                }
114                Expr::PatchDef(patch_def) => {
115                    self.check_expr(patch_def.base.as_ref());
116                    // TODO: grow
117                    for def in patch_def.methods.iter() {
118                        self.check_expr(def);
119                    }
120                }
121                Expr::Call(call) => {
122                    for parg in call.args.pos_args.iter() {
123                        self.check_expr(&parg.expr);
124                    }
125                    for kwarg in call.args.kw_args.iter() {
126                        self.check_expr(&kwarg.expr);
127                    }
128                }
129                Expr::BinOp(bin) => {
130                    self.check_expr(&bin.lhs);
131                    self.check_expr(&bin.rhs);
132                }
133                Expr::UnaryOp(unary) => {
134                    self.check_expr(&unary.expr);
135                }
136                Expr::Accessor(_) | Expr::Literal(_) => {}
137                Expr::List(list) => match list {
138                    List::Normal(lis) => {
139                        for elem in lis.elems.pos_args.iter() {
140                            self.check_expr(&elem.expr);
141                        }
142                    }
143                    List::WithLength(lis) => {
144                        self.check_expr(&lis.elem);
145                        if let Some(len) = &lis.len {
146                            self.check_expr(len);
147                        }
148                    }
149                    List::Comprehension(lis) => {
150                        self.check_expr(&lis.elem);
151                        self.check_expr(&lis.guard);
152                    }
153                },
154                Expr::Tuple(tuple) => match tuple {
155                    Tuple::Normal(tuple) => {
156                        for elem in tuple.elems.pos_args.iter() {
157                            self.check_expr(&elem.expr);
158                        }
159                    }
160                },
161                Expr::Record(rec) => {
162                    self.path_stack
163                        .push(Visibility::private(Str::ever("<record>")));
164                    self.block_stack.push(Instant);
165                    for attr in rec.attrs.iter() {
166                        self.check_def(attr);
167                    }
168                    self.path_stack.pop();
169                    self.block_stack.pop();
170                }
171                Expr::Set(set) => match set {
172                    Set::Normal(set) => {
173                        for elem in set.elems.pos_args.iter() {
174                            self.check_expr(&elem.expr);
175                        }
176                    }
177                    Set::WithLength(set) => {
178                        self.check_expr(&set.elem);
179                        self.check_expr(&set.len);
180                    }
181                },
182                Expr::Dict(dict) => match dict {
183                    Dict::Normal(dict) => {
184                        for kv in dict.kvs.iter() {
185                            self.check_expr(&kv.key);
186                            self.check_expr(&kv.value);
187                        }
188                    }
189                    other => todo!("{other}"),
190                },
191                Expr::TypeAsc(tasc) => {
192                    self.check_expr(&tasc.expr);
193                }
194                Expr::Lambda(lambda) => {
195                    let is_proc = lambda.is_procedural();
196                    if is_proc {
197                        self.path_stack
198                            .push(Visibility::private(Str::ever("<lambda!>")));
199                        self.block_stack.push(Proc);
200                    } else {
201                        self.path_stack
202                            .push(Visibility::private(Str::ever("<lambda>")));
203                        self.block_stack.push(Func);
204                    }
205                    lambda.body.iter().for_each(|chunk| self.check_expr(chunk));
206                    self.path_stack.pop();
207                    self.block_stack.pop();
208                }
209                Expr::ReDef(_)
210                | Expr::Code(_)
211                | Expr::Compound(_)
212                | Expr::Import(_)
213                | Expr::Dummy(_) => {}
214            }
215        }
216        log!(info "the side-effects checking process has completed, found errors: {}{RESET}", self.errs.len());
217        if self.errs.is_empty() {
218            Ok(hir)
219        } else {
220            Err((hir, self.errs))
221        }
222    }
223
224    fn check_params(&mut self, params: &Params) {
225        for nd_param in params.non_defaults.iter() {
226            if nd_param.vi.t.is_procedure() && !nd_param.inspect().unwrap().ends_with('!') {
227                self.errs.push(EffectError::proc_assign_error(
228                    self.cfg.input.clone(),
229                    line!() as usize,
230                    nd_param.raw.pat.loc(),
231                    self.full_path(),
232                ));
233            }
234        }
235        if let Some(var_arg) = params.var_params.as_deref() {
236            if var_arg.vi.t.is_procedure() && !var_arg.inspect().unwrap().ends_with('!') {
237                self.errs.push(EffectError::proc_assign_error(
238                    self.cfg.input.clone(),
239                    line!() as usize,
240                    var_arg.raw.pat.loc(),
241                    self.full_path(),
242                ));
243            }
244        }
245        for d_param in params.defaults.iter() {
246            if d_param.sig.vi.t.is_procedure() && !d_param.inspect().unwrap().ends_with('!') {
247                self.errs.push(EffectError::proc_assign_error(
248                    self.cfg.input.clone(),
249                    line!() as usize,
250                    d_param.sig.raw.pat.loc(),
251                    self.full_path(),
252                ));
253            }
254            self.check_expr(&d_param.default_val);
255        }
256    }
257
258    fn check_def(&mut self, def: &Def) {
259        let is_procedural = def.sig.is_procedural();
260        let is_subr = def.sig.is_subr();
261        let is_const = def.sig.is_const();
262        if is_subr {
263            let name_and_vis = Visibility::new(def.sig.vis().clone(), def.sig.inspect().clone());
264            self.path_stack.push(name_and_vis);
265        }
266        match (is_procedural, is_subr, is_const) {
267            (true, true, true) => {
268                panic!("user-defined constant procedures are not allowed");
269            }
270            (true, true, false) => {
271                self.block_stack.push(Proc);
272            }
273            (_, false, false) => {
274                self.block_stack.push(Instant);
275            }
276            (false, true, true) => {
277                self.block_stack.push(ConstFunc);
278            }
279            (false, true, false) => {
280                self.block_stack.push(Func);
281            }
282            (_, false, true) => {
283                self.block_stack.push(ConstInstant);
284            }
285        }
286        if let Signature::Subr(sig) = &def.sig {
287            self.check_params(&sig.params);
288        }
289        let last_idx = def.body.block.len().saturating_sub(1);
290        for (i, chunk) in def.body.block.iter().enumerate() {
291            self.check_expr(chunk);
292            // e.g. `echo = print!`
293            if i == last_idx
294                && self.block_stack.last().unwrap() == &Instant
295                && !def.sig.is_procedural()
296                && chunk.t().is_procedure()
297            {
298                self.errs.push(EffectError::proc_assign_error(
299                    self.cfg.input.clone(),
300                    line!() as usize,
301                    def.sig.loc(),
302                    self.full_path(),
303                ));
304            }
305        }
306        if is_subr {
307            self.path_stack.pop();
308        }
309        self.block_stack.pop();
310    }
311
312    /// check if `expr` has side-effects / purity violations.
313    ///
314    /// returns effects, purity violations will be appended to `self.errs`.
315    ///
316    /// causes side-effects:
317    /// ```python
318    /// p!() // 1 side-effect
319    /// p!(q!()) // 2 side-effects
320    /// x =
321    ///     y = r!()
322    ///     y + 1 // 1 side-effect
323    /// ```
324    /// causes no side-effects:
325    /// ```python
326    /// q! = p!
327    /// y = f(p!)
328    /// ```
329    /// purity violation:
330    /// ```python
331    /// for iter, i -> print! i
332    /// ```
333    fn check_expr(&mut self, expr: &Expr) {
334        match expr {
335            Expr::Literal(_) => {}
336            Expr::Def(def) => {
337                self.check_def(def);
338            }
339            Expr::ClassDef(class_def) => {
340                if let Some(req_sup) = &class_def.require_or_sup {
341                    self.check_expr(req_sup);
342                }
343                for def in class_def.all_methods() {
344                    self.check_expr(def);
345                }
346            }
347            Expr::PatchDef(patch_def) => {
348                self.check_expr(patch_def.base.as_ref());
349                for def in patch_def.methods.iter() {
350                    self.check_expr(def);
351                }
352            }
353            Expr::List(list) => match list {
354                List::Normal(lis) => {
355                    for elem in lis.elems.pos_args.iter() {
356                        self.check_expr(&elem.expr);
357                    }
358                }
359                List::WithLength(lis) => {
360                    self.check_expr(&lis.elem);
361                    if let Some(len) = &lis.len {
362                        self.check_expr(len);
363                    }
364                }
365                List::Comprehension(lis) => {
366                    self.check_expr(&lis.elem);
367                    self.check_expr(&lis.guard);
368                }
369            },
370            Expr::Tuple(tuple) => match tuple {
371                Tuple::Normal(tup) => {
372                    for arg in tup.elems.pos_args.iter() {
373                        self.check_expr(&arg.expr);
374                    }
375                }
376            },
377            Expr::Record(record) => {
378                self.path_stack
379                    .push(Visibility::private(Str::ever("<record>")));
380                self.block_stack.push(Instant);
381                for attr in record.attrs.iter() {
382                    self.check_def(attr);
383                }
384                self.path_stack.pop();
385                self.block_stack.pop();
386            }
387            Expr::Set(set) => match set {
388                Set::Normal(set) => {
389                    for elem in set.elems.pos_args.iter() {
390                        self.check_expr(&elem.expr);
391                    }
392                }
393                Set::WithLength(set) => {
394                    self.check_expr(&set.elem);
395                    self.check_expr(&set.len);
396                }
397            },
398            Expr::Dict(dict) => match dict {
399                Dict::Normal(dict) => {
400                    for kv in dict.kvs.iter() {
401                        self.check_expr(&kv.key);
402                        self.check_expr(&kv.value);
403                    }
404                }
405                other => todo!("{other}"),
406            },
407            Expr::Call(call) => {
408                self.constructor_destructor_check(call);
409                self.check_expr(&call.obj);
410                if (call.obj.t().is_procedure()
411                    || call
412                        .attr_name
413                        .as_ref()
414                        .map(|name| name.is_procedural())
415                        .unwrap_or(false))
416                    && !self.in_context_effects_allowed()
417                {
418                    self.errs.push(EffectError::has_effect(
419                        self.cfg.input.clone(),
420                        line!() as usize,
421                        expr,
422                        self.full_path(),
423                    ));
424                }
425                call.args
426                    .pos_args
427                    .iter()
428                    .for_each(|parg| self.check_expr(&parg.expr));
429                call.args
430                    .kw_args
431                    .iter()
432                    .for_each(|kwarg| self.check_expr(&kwarg.expr));
433            }
434            Expr::UnaryOp(unary) => {
435                self.check_expr(&unary.expr);
436            }
437            Expr::BinOp(bin) => {
438                self.check_expr(&bin.lhs);
439                self.check_expr(&bin.rhs);
440                if (bin.op.kind == TokenKind::IsOp || bin.op.kind == TokenKind::IsNotOp)
441                    && !self.in_context_effects_allowed()
442                {
443                    self.errs.push(EffectError::has_effect(
444                        self.cfg.input.clone(),
445                        line!() as usize,
446                        expr,
447                        self.full_path(),
448                    ));
449                }
450            }
451            Expr::Lambda(lambda) => {
452                let is_proc = lambda.is_procedural();
453                if is_proc {
454                    self.path_stack
455                        .push(Visibility::private(Str::ever("<lambda!>")));
456                    self.block_stack.push(Proc);
457                } else {
458                    self.path_stack
459                        .push(Visibility::private(Str::ever("<lambda>")));
460                    self.block_stack.push(Func);
461                }
462                self.check_params(&lambda.params);
463                lambda.body.iter().for_each(|chunk| self.check_expr(chunk));
464                self.path_stack.pop();
465                self.block_stack.pop();
466            }
467            Expr::TypeAsc(type_asc) => {
468                self.check_expr(&type_asc.expr);
469            }
470            Expr::Accessor(acc) => {
471                if !self.in_context_effects_allowed()
472                    && !acc.var_info().is_parameter()
473                    && acc.ref_t().is_mut_type()
474                    && acc.root_obj().map_or(true, |obj| !obj.ref_t().is_ref())
475                    && acc.var_info().def_namespace() != &self.full_path()
476                {
477                    self.errs.push(EffectError::touch_mut_error(
478                        self.cfg.input.clone(),
479                        line!() as usize,
480                        expr,
481                        self.full_path(),
482                    ));
483                }
484            }
485            Expr::ReDef(_)
486            | Expr::Code(_)
487            | Expr::Compound(_)
488            | Expr::Import(_)
489            | Expr::Dummy(_) => {}
490        }
491    }
492
493    fn constructor_destructor_check(&mut self, call: &Call) {
494        let Some(gen_t) = call.signature_t().and_then(|sig| sig.return_t()) else {
495            return;
496        };
497        // the call generates a new instance
498        // REVIEW: is this correct?
499        if !self.in_context_effects_allowed()
500            && call
501                .signature_t()
502                .is_some_and(|sig| sig.param_ts().iter().all(|p| !p.contains_type(gen_t)))
503        {
504            if let Some(typ_ctx) = self.ctx.get_nominal_type_ctx(gen_t) {
505                if typ_ctx.get_method_kv("__init__!").is_some()
506                    || typ_ctx.get_method_kv("__del__!").is_some()
507                {
508                    self.errs.push(EffectError::constructor_destructor_error(
509                        self.cfg.input.clone(),
510                        line!() as usize,
511                        call.loc(),
512                        self.full_path(),
513                    ));
514                }
515            }
516        }
517    }
518
519    pub(crate) fn is_impure(expr: &Expr) -> bool {
520        match expr {
521            Expr::Call(call) => {
522                call.ref_t().is_procedure()
523                    || call
524                        .args
525                        .pos_args
526                        .iter()
527                        .any(|parg| Self::is_impure(&parg.expr))
528                    || call
529                        .args
530                        .var_args
531                        .iter()
532                        .any(|varg| Self::is_impure(&varg.expr))
533                    || call
534                        .args
535                        .kw_args
536                        .iter()
537                        .any(|kwarg| Self::is_impure(&kwarg.expr))
538            }
539            Expr::BinOp(bin) => Self::is_impure(&bin.lhs) || Self::is_impure(&bin.rhs),
540            Expr::UnaryOp(unary) => Self::is_impure(&unary.expr),
541            Expr::List(lis) => match lis {
542                List::Normal(lis) => lis
543                    .elems
544                    .pos_args
545                    .iter()
546                    .any(|elem| Self::is_impure(&elem.expr)),
547                List::WithLength(lis) => {
548                    Self::is_impure(&lis.elem)
549                        || lis.len.as_ref().is_some_and(|len| Self::is_impure(len))
550                }
551                _ => todo!(),
552            },
553            Expr::Tuple(tup) => match tup {
554                Tuple::Normal(tup) => tup
555                    .elems
556                    .pos_args
557                    .iter()
558                    .any(|elem| Self::is_impure(&elem.expr)),
559            },
560            Expr::Set(set) => match set {
561                Set::Normal(set) => set
562                    .elems
563                    .pos_args
564                    .iter()
565                    .any(|elem| Self::is_impure(&elem.expr)),
566                Set::WithLength(set) => Self::is_impure(&set.elem) || Self::is_impure(&set.len),
567            },
568            Expr::Dict(dict) => match dict {
569                Dict::Normal(dict) => dict
570                    .kvs
571                    .iter()
572                    .any(|kv| Self::is_impure(&kv.key) || Self::is_impure(&kv.value)),
573                _ => todo!(),
574            },
575            Expr::Lambda(lambda) => {
576                lambda.op.is_procedural() || lambda.body.iter().any(Self::is_impure)
577            }
578            Expr::Def(def) => def.sig.is_procedural() || def.body.block.iter().any(Self::is_impure),
579            /*
580            Expr::ClassDef(class_def) => {
581                class_def.methods.iter().any(|def| Self::is_impure(def))
582            }
583            Expr::PatchDef(patch_def) => {
584                patch_def.methods.iter().any(|def| Self::is_impure(def))
585            }*/
586            Expr::Code(block) | Expr::Compound(block) => block.iter().any(Self::is_impure),
587            _ => false,
588        }
589    }
590
591    pub(crate) fn is_pure(expr: &Expr) -> bool {
592        !Self::is_impure(expr)
593    }
594}