263 lines
10 KiB
Text
263 lines
10 KiB
Text
/-
|
||
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
|