{-# language DataKinds #-} module FixMutableDefaultArguments where import Control.Lens.Cons (_head) import Control.Lens.Fold ((^..), (^?), filtered, folded, anyOf, has) import Control.Lens.Getter ((^.)) import Control.Lens.Review ((#)) import Control.Lens.Setter ((.~), (%~)) import Control.Monad (guard) import Data.Function ((&)) import Data.Semigroup ((<>)) import Language.Python.DSL import Language.Python.Optics import Language.Python.Syntax.Expr (Expr(..), _Exprs) {- I want to write a function that fixes the 'mutable default argument' pattern. It takes a function like this: def a(b=[]): b.push(1) to this: def a(b=None): if b is None: b = [] b.push(1) if the parameter is a 'mutable' thing. -} fixMutableDefaultArguments :: Raw Statement -> Maybe (Raw Statement) fixMutableDefaultArguments input = do {- Firstly, this transformation only applies to function definitions. So we use the '_Fundef' prism to pull out the function definition from a statement. -} function <- input ^? _Fundef let {- I want the parameters of the function as a list. I can use the 'parameters' lens to get at them. -} paramsList = function ^.. parameters.folded {- I also want to know which parameters meet the 'mutable default argument' pattern. The '_KeywordParam' prism only matches keyword parameters, and '_kpExpr' is the field accessor for the right hand side of an '=' in a keyword parameter. This expression gets 'all the keyword parameters that have mutable value on their RHS' -} targetParams = paramsList ^.. folded._KeywordParam.filtered (isMutable._kpExpr) {- If the list of targetParams is empty, then we don't need to do the transformation -} guard $ has _head targetParams let {- Let's generate some 'if' statements -} conditionalAssignments = {- for each 'param' in our list of keyword parameters -} (\param -> let {- let be the left hand side of the '=' -} pName = var_ (param ^. kpName.identValue) {- let be the right hand side of the '=' -} pValue = param ^. kpExpr in {- output a new line, which says... -} line_ $ {- if is None: = -} if_ (pName `is_` none_) [ line_ (pName .= pValue) ]) <$> targetParams {- For each parameter in the original parameter list, set the right hand sides of the target parameters to 'None', but leave all of the others as they were. -} newparams = paramsList & traverse._KeywordParam.kpExpr.filtered isMutable .~ none_ pure $ {- Return a new function defition statement -} _Fundef # {- that consists of the original function -} (function & {- with its problem parameters set to 'None' -} parameters_ .~ newparams & {- and for each problem parameter, there is a corresponding if statement at the start of the function definition -} body_ %~ (conditionalAssignments <>)) where {- This function decides whether or not an expression is mutable -} isMutable :: Raw Expr -> Bool isMutable Unit{} = False isMutable None{} = False isMutable Ellipsis{} = False isMutable Lambda{} = False isMutable Float{} = False isMutable Imag{} = False isMutable Int{} = False isMutable Bool{} = False isMutable String{} = False isMutable List{} = True isMutable ListComp{} = True isMutable Deref{} = True isMutable Call{} = True isMutable BinOp{} = True isMutable UnOp{} = True isMutable Not{} = True isMutable DictComp{} = True isMutable Dict{} = True isMutable Ident{} = True isMutable Yield{} = True isMutable Await{} = True isMutable YieldFrom{} = True isMutable SetComp{} = True isMutable Set{} = True isMutable Subscript{} = True isMutable Generator{} = True isMutable (Ternary _ _ _ a _ b) = isMutable a || isMutable b isMutable (Parens _ _ a _) = isMutable a isMutable (Tuple _ a _ as) = {- Tuples contain many expressions, and are mutable if any of the sub-expressions are mutable. The '_Exprs' traversal can get at all these sub-expressions -} anyOf _Exprs isMutable a || anyOf (folded.folded._Exprs) isMutable as