/- Copyright (c) 2024 Lean FRO, LLC. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Authors: Kim Morrison -/ prelude import Lean.Elab.Command import Lean.Server.InfoUtils set_option linter.missingDocs true -- keep it documented /-! This file defines style linters for the `List`/`Array`/`Vector` modules. Currently, we do not anticipate that they will be useful elsewhere. -/ namespace Lean.Linter.List /-- `set_option linter.indexVariables true` enables a strict linter that validates that the only variables appearing as an index (e.g. in `xs[i]` or `xs.take i`) are `i`, `j`, or `k`, and similarly that the only variables appearing as a width (e.g. in `List.replicate n a` or `Vector α n`) are `n` or `m`. -/ register_builtin_option linter.indexVariables : Bool := { defValue := false descr := "Validate that variables appearing as an index (e.g. in `xs[i]` or `xs.take i`) are only `i`, `j`, or `k`." } /-- `set_option linter.listVariables true` enables a strict linter that validates that all `List`/`Array`/`Vector` variables use standardized names. -/ register_builtin_option linter.listVariables : Bool := { defValue := false descr := "Validate that all `List`/`Array`/`Vector` variables use allowed names." } open Lean Elab Command /-- Return the syntax for all expressions in which an `fvarId` appears as a "numerical index", along with the user name of that `fvarId`. -/ def numericalIndices (t : InfoTree) : List (Syntax × Name) := (t.deepestNodes fun _ info _ => do let stx := info.stx if let .ofTermInfo info := info then let idxs := match_expr info.expr with | GetElem.getElem _ _ _ _ _ _ i _ => [i] | GetElem?.getElem? _ _ _ _ _ _ i => [i] | List.take _ i _ => [i] | List.drop _ i _ => [i] | List.set _ _ i _ => [i] | List.insertIdx _ _ i _ => [i] | List.eraseIdx _ _ i => [i] | List.modify _ _ i _ => [i] | List.zipIdx _ _ i => [i] | Array.extract _ _ i j => [i, j] | Array.take _ _ i => [i] | Array.drop _ _ i => [i] | Array.shrink _ _ i => [i] | Array.set _ _ i _ _ => [i] | Array.uset _ _ i _ _ => [i] | Array.setIfInBounds _ _ i _ => [i] | Array.insertIdx _ _ i _ _ => [i] | Array.insertIdxIfInBounds _ _ i _ => [i] | Array.insertIdx! _ _ i _ => [i] | Array.eraseIdx _ _ i _ => [i] | Array.eraseIdxIfInBounds _ _ i _ => [i] | Array.eraseIdx! _ _ i => [i] | Array.modify _ _ i _ => [i] | Array.zipIdx _ _ i => [i] | Array.swap _ _ i j _ => [i, j] | Vector.extract _ _ _ i j => [i, j] | Vector.take _ _ _ i => [i] | Vector.drop _ _ _ i => [i] | Vector.shrink _ _ _ i => [i] | Vector.set _ _ _ i _ _ => [i] | Vector.setIfInBounds _ _ _ i _ => [i] | Vector.insertIdx _ _ _ i _ _ => [i] | Vector.eraseIdx _ _ _ i _ => [i] | Vector.insertIdx! _ _ _ i _ => [i] | Vector.eraseIdx! _ _ _ i => [i] | Vector.zipIdx _ _ _ i => [i] | Vector.swap _ _ _ i j _ => [i, j] | _ => [] match idxs with | [] => none | _ => idxs.filterMap fun i => match i with | .fvar i => match info.lctx.find? i with | some ldecl => some (stx, ldecl.userName) | none => none | _ => none else none).flatten /-- Return the syntax for all expressions in which an `fvarId` appears as a "numerical width", along with the user name of that `fvarId`. -/ def numericalWidths (t : InfoTree) : List (Syntax × Name) := (t.deepestNodes fun _ info _ => do let stx := info.stx if let .ofTermInfo info := info then let idxs := match_expr info.expr with | List.replicate _ n _ => [n] | Array.replicate _ n _ => [n] | Vector.replicate _ n _ => [n] | List.range n => [n] | List.range' _ n _ => [n] | Array.range n => [n] | Array.range' _ n _ => [n] | Vector.range n => [n] | Vector.range' _ n _ => [n] | Vector _ n => [n] | _ => [] match idxs with | [] => none | _ => idxs.filterMap fun i => match i with | .fvar i => match info.lctx.find? i with | some ldecl => some (stx, ldecl.userName) | none => none | _ => none else none).flatten /-- Return the syntax for all expressions in which an `fvarId` appears as a "BitVec width", along with the user name of that `fvarId`. -/ def bitVecWidths (t : InfoTree) : List (Syntax × Name) := (t.deepestNodes fun _ info _ => do let stx := info.stx if let .ofTermInfo info := info then let idxs := match_expr info.expr with | BitVec w => [w] | _ => [] match idxs with | [] => none | _ => idxs.filterMap fun i => match i with | .fvar i => match info.lctx.find? i with | some ldecl => some (stx, ldecl.userName) | none => none | _ => none else none).flatten /-- Strip optional suffixes from a binder name. -/ def stripBinderName (s : String) : String := s.stripSuffix "'" |>.stripSuffix "₁" |>.stripSuffix "₂" |>.stripSuffix "₃" |>.stripSuffix "₄" /-- Allowed names for index variables. -/ def allowedIndices : List String := ["i", "j", "k", "start", "stop", "step"] /-- Allowed names for width variables. -/ def allowedWidths : List String := ["n", "m", "k", "l", "size"] /-- Allowed names for BitVec width variables. -/ def allowedBitVecWidths : List String := ["w"] /-- A linter which validates that the only variables used as "indices" (e.g. in `xs[i]` or `xs.take i`) are `i`, `j`, or `k`. -/ def indexLinter : Linter where run := withSetOptionIn fun stx => do -- We intentionally do not use `getLinterValue` here, as we do *not* want to opt in to `linter.all`. unless (← getOptions).get linter.indexVariables.name false do return if (← get).messages.hasErrors then return if ! (← getInfoState).enabled then return for t in ← getInfoTrees do if let .context _ _ := t then -- Only consider info trees with top-level context for (idxStx, n) in numericalIndices t do if let .str _ n := n then if !allowedIndices.contains (stripBinderName n) then Linter.logLint linter.indexVariables idxStx m!"Forbidden variable appearing as an index: use `i`, `j`, or `k`: {n}" for (idxStx, n) in numericalWidths t do if let .str _ n := n then if !allowedWidths.contains (stripBinderName n) then Linter.logLint linter.indexVariables idxStx m!"Forbidden variable appearing as a width: use `n` or `m`: {n}" for (idxStx, n) in bitVecWidths t do if let .str _ n := n then if !allowedBitVecWidths.contains (stripBinderName n) then Linter.logLint linter.indexVariables idxStx m!"Forbidden variable appearing as a BitVec width: use `w`: {n}" builtin_initialize addLinter indexLinter /-- Allowed names for `List` variables. -/ def allowedListNames : List String := ["l", "r", "s", "t", "tl", "ws", "xs", "ys", "zs", "as", "bs", "cs", "ds", "acc"] /-- Allowed names for `Array` variables. -/ def allowedArrayNames : List String := ["ws", "xs", "ys", "zs", "as", "bs", "cs", "ds", "acc"] /-- Allowed names for `Vector` variables. -/ def allowedVectorNames : List String := ["ws", "xs", "ys", "zs", "as", "bs", "cs", "ds", "acc"] /-- Find all binders appearing in the given info tree. -/ def binders (t : InfoTree) (p : Expr → Bool := fun _ => true) : IO (List (Syntax × Name × Expr)) := t.collectTermInfoM fun ctx ti => do if ti.isBinder then do -- Something is wrong here: sometimes `inferType` fails with an unknown fvar error, -- despite passing the local context here. -- We fail quietly by returning a `Unit` type. let ty ← ctx.runMetaM ti.lctx do instantiateMVars (← (Meta.inferType ti.expr) <|> pure (.const `Unit [])) if p ty then if let .fvar i := ti.expr then match ti.lctx.find? i with | some ldecl => return some (ti.stx, ldecl.userName, ty) | none => return none else return none else return none else return none /-- A linter which validates that all `List`/`Array`/`Vector` variables use allowed names. -/ def listVariablesLinter : Linter where run := withSetOptionIn fun stx => do unless (← getOptions).get linter.listVariables.name false do return if (← get).messages.hasErrors then return if ! (← getInfoState).enabled then return for t in ← getInfoTrees do if let .context _ _ := t then -- Only consider info trees with top-level context let binders ← binders t for (stx, n, ty) in binders.filter fun (_, _, ty) => ty.isAppOf `List do if let .str _ n := n then let n := stripBinderName n if !allowedListNames.contains n then -- Allow `L` or `xss` for `List (List α)` or `List (Array α)` unless ((ty.getArg! 0).isAppOf `List || (ty.getArg! 0).isAppOf `Array) && (n == "L" || n == "xss") do Linter.logLint linter.listVariables stx m!"Forbidden variable appearing as a `List` name: {n}" for (stx, n, ty) in binders.filter fun (_, _, ty) => ty.isAppOf `Array do if let .str _ n := n then let n := stripBinderName n if !allowedArrayNames.contains n then -- Allow `xss` for `Array (Array α)` or `Array (Vector α)` unless ((ty.getArg! 0).isAppOf `Array || (ty.getArg! 0).isAppOf `Vector) && n == "xss" do Linter.logLint linter.listVariables stx m!"Forbidden variable appearing as a `Array` name: {n}" for (stx, n, ty) in binders.filter fun (_, _, ty) => ty.isAppOf `Vector do if let .str _ n := n then let n := stripBinderName n if !allowedVectorNames.contains n then -- Allow `xss` for `Vector (Vector α)` unless (ty.getArg! 0).isAppOf `Vector && n == "xss" do Linter.logLint linter.listVariables stx m!"Forbidden variable appearing as a `Vector` name: {n}" builtin_initialize addLinter listVariablesLinter end Lean.Linter.List