2023-03-30 16:48:39 +03:00
|
|
|
(* This file is part of the Catala compiler, a specification language for tax
|
|
|
|
and social benefits computation rules. Copyright (C) 2023 Inria, contributor:
|
|
|
|
Denis Merigoux <denis.merigoux@inria.fr>
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
|
|
use this file except in compliance with the License. You may obtain a copy of
|
|
|
|
the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
|
|
License for the specific language governing permissions and limitations under
|
|
|
|
the License. *)
|
|
|
|
|
|
|
|
open Shared_ast
|
|
|
|
open Ast
|
|
|
|
open Catala_utils
|
|
|
|
|
|
|
|
(** If the variable is not an input, then it should be defined somewhere. *)
|
2023-03-30 19:52:29 +03:00
|
|
|
let detect_empty_definitions (p : program) : unit =
|
2023-03-30 16:48:39 +03:00
|
|
|
ScopeName.Map.iter
|
2023-03-30 19:52:29 +03:00
|
|
|
(fun (scope_name : ScopeName.t) scope ->
|
2023-04-18 11:31:44 +03:00
|
|
|
ScopeDef.Map.iter
|
2023-03-30 16:48:39 +03:00
|
|
|
(fun scope_def_key scope_def ->
|
2023-03-30 19:52:29 +03:00
|
|
|
if
|
|
|
|
(match scope_def_key with ScopeDef.Var _ -> true | _ -> false)
|
|
|
|
&& RuleName.Map.is_empty scope_def.scope_def_rules
|
|
|
|
&& (not scope_def.scope_def_is_condition)
|
|
|
|
&&
|
2023-05-17 16:44:57 +03:00
|
|
|
match Mark.remove scope_def.scope_def_io.io_input with
|
2023-05-26 17:03:26 +03:00
|
|
|
| NoInput -> true
|
2023-03-30 19:52:29 +03:00
|
|
|
| _ -> false
|
|
|
|
then
|
2023-06-13 12:27:45 +03:00
|
|
|
Message.emit_spanned_warning
|
2023-03-30 19:52:29 +03:00
|
|
|
(ScopeDef.get_position scope_def_key)
|
2023-06-07 19:15:14 +03:00
|
|
|
"In scope @{<yellow>\"%a\"@}, the variable @{<yellow>\"%a\"@} is \
|
|
|
|
declared but never defined; did you forget something?"
|
2023-07-12 12:48:46 +03:00
|
|
|
ScopeName.format scope_name Ast.ScopeDef.format scope_def_key)
|
2023-03-31 18:56:45 +03:00
|
|
|
scope.scope_defs)
|
|
|
|
p.program_scopes
|
|
|
|
|
2023-04-14 18:38:10 +03:00
|
|
|
(* To detect rules that have the same justification and conclusion, we create a
|
|
|
|
set data structure with an appropriate comparison function *)
|
|
|
|
module RuleExpressionsMap = Map.Make (struct
|
|
|
|
type t = rule
|
|
|
|
|
|
|
|
let compare x y =
|
|
|
|
let xj, xj_mark = x.rule_just in
|
|
|
|
let yj, yj_mark = y.rule_just in
|
|
|
|
let just =
|
|
|
|
Bindlib.unbox
|
|
|
|
(Bindlib.box_apply2
|
|
|
|
(fun xj yj -> Expr.compare (xj, xj_mark) (yj, yj_mark))
|
|
|
|
xj yj)
|
|
|
|
in
|
|
|
|
if just = 0 then
|
|
|
|
let xc, xc_mark = x.rule_cons in
|
|
|
|
let yc, yc_mark = y.rule_cons in
|
|
|
|
Bindlib.unbox
|
|
|
|
(Bindlib.box_apply2
|
|
|
|
(fun xc yc -> Expr.compare (xc, xc_mark) (yc, yc_mark))
|
|
|
|
xc yc)
|
|
|
|
else just
|
2023-07-12 12:48:46 +03:00
|
|
|
|
|
|
|
let format ppf r = RuleName.format ppf r.rule_id
|
2023-04-14 18:38:10 +03:00
|
|
|
end)
|
|
|
|
|
|
|
|
let detect_identical_rules (p : program) : unit =
|
|
|
|
ScopeName.Map.iter
|
|
|
|
(fun _ scope ->
|
2023-04-27 13:18:50 +03:00
|
|
|
ScopeDef.Map.iter
|
2023-04-14 18:38:10 +03:00
|
|
|
(fun _ scope_def ->
|
|
|
|
let rules_seen =
|
|
|
|
RuleName.Map.fold
|
|
|
|
(fun _ rule rules_seen ->
|
|
|
|
RuleExpressionsMap.update rule
|
|
|
|
(fun l ->
|
|
|
|
let x =
|
|
|
|
( None,
|
|
|
|
Pos.overwrite_law_info
|
|
|
|
(snd (RuleName.get_info rule.rule_id))
|
|
|
|
(Pos.get_law_info (Expr.pos rule.rule_just)) )
|
|
|
|
in
|
|
|
|
match l with None -> Some [x] | Some l -> Some (x :: l))
|
|
|
|
rules_seen)
|
|
|
|
scope_def.scope_def_rules RuleExpressionsMap.empty
|
|
|
|
in
|
|
|
|
RuleExpressionsMap.iter
|
|
|
|
(fun _ pos ->
|
|
|
|
if List.length pos > 1 then
|
2023-06-13 12:27:45 +03:00
|
|
|
Message.emit_multispanned_warning pos
|
2023-04-14 18:38:10 +03:00
|
|
|
"These %s have identical justifications and consequences; is \
|
|
|
|
it a mistake?"
|
|
|
|
(if scope_def.scope_def_is_condition then "rules"
|
|
|
|
else "definitions"))
|
|
|
|
rules_seen)
|
|
|
|
scope.scope_defs)
|
|
|
|
p.program_scopes
|
|
|
|
|
2023-03-30 19:52:29 +03:00
|
|
|
let detect_unused_struct_fields (p : program) : unit =
|
2023-04-21 17:07:36 +03:00
|
|
|
(* TODO: this analysis should be finer grained: a false negative is if the
|
|
|
|
field is used to define itself, for passing data around but that never gets
|
|
|
|
really used or defined. *)
|
2023-03-30 19:52:29 +03:00
|
|
|
let struct_fields_used =
|
|
|
|
Ast.fold_exprs
|
|
|
|
~f:(fun struct_fields_used e ->
|
|
|
|
let rec structs_fields_used_expr e struct_fields_used =
|
2023-05-17 16:44:57 +03:00
|
|
|
match Mark.remove e with
|
2023-08-16 01:04:45 +03:00
|
|
|
| EDStructAccess
|
|
|
|
{ name_opt = Some name; e = e_struct; field; path = _ } ->
|
2023-03-30 19:52:29 +03:00
|
|
|
let field =
|
2023-03-31 12:47:44 +03:00
|
|
|
StructName.Map.find name
|
2023-04-18 15:39:38 +03:00
|
|
|
(Ident.Map.find field p.program_ctx.ctx_struct_fields)
|
2023-03-30 19:52:29 +03:00
|
|
|
in
|
|
|
|
StructField.Set.add field
|
|
|
|
(structs_fields_used_expr e_struct struct_fields_used)
|
|
|
|
| EStruct { name = _; fields } ->
|
|
|
|
StructField.Map.fold
|
|
|
|
(fun field e_field struct_fields_used ->
|
|
|
|
StructField.Set.add field
|
|
|
|
(structs_fields_used_expr e_field struct_fields_used))
|
|
|
|
fields struct_fields_used
|
2023-04-03 14:42:14 +03:00
|
|
|
| _ -> Expr.shallow_fold structs_fields_used_expr e struct_fields_used
|
2023-03-30 19:52:29 +03:00
|
|
|
in
|
|
|
|
structs_fields_used_expr e struct_fields_used)
|
|
|
|
~init:StructField.Set.empty p
|
|
|
|
in
|
|
|
|
let scope_out_structs_fields =
|
|
|
|
ScopeName.Map.fold
|
|
|
|
(fun _ out_struct acc ->
|
|
|
|
ScopeVar.Map.fold
|
|
|
|
(fun _ field acc -> StructField.Set.add field acc)
|
|
|
|
out_struct.out_struct_fields acc)
|
|
|
|
p.program_ctx.ctx_scopes StructField.Set.empty
|
|
|
|
in
|
|
|
|
StructName.Map.iter
|
2023-08-10 17:52:39 +03:00
|
|
|
(fun s_name (path, fields) ->
|
|
|
|
if path <> [] then ()
|
|
|
|
else if
|
2023-03-31 18:56:45 +03:00
|
|
|
(not (StructField.Map.is_empty fields))
|
|
|
|
&& StructField.Map.for_all
|
|
|
|
(fun field _ ->
|
|
|
|
(not (StructField.Set.mem field struct_fields_used))
|
|
|
|
&& not (StructField.Set.mem field scope_out_structs_fields))
|
|
|
|
fields
|
|
|
|
then
|
2023-06-13 12:27:45 +03:00
|
|
|
Message.emit_spanned_warning
|
2023-03-31 18:56:45 +03:00
|
|
|
(snd (StructName.get_info s_name))
|
2023-06-07 19:15:14 +03:00
|
|
|
"The structure @{<yellow>\"%a\"@} is never used; maybe it's \
|
|
|
|
unnecessary?"
|
2023-07-12 12:48:46 +03:00
|
|
|
StructName.format s_name
|
2023-03-31 18:56:45 +03:00
|
|
|
else
|
|
|
|
StructField.Map.iter
|
|
|
|
(fun field _ ->
|
|
|
|
if
|
|
|
|
(not (StructField.Set.mem field struct_fields_used))
|
|
|
|
&& not (StructField.Set.mem field scope_out_structs_fields)
|
|
|
|
then
|
2023-06-13 12:27:45 +03:00
|
|
|
Message.emit_spanned_warning
|
2023-03-31 18:56:45 +03:00
|
|
|
(snd (StructField.get_info field))
|
2023-06-07 19:15:14 +03:00
|
|
|
"The field @{<yellow>\"%a\"@} of struct @{<yellow>\"%a\"@} is \
|
|
|
|
never used; maybe it's unnecessary?"
|
2023-07-12 12:48:46 +03:00
|
|
|
StructField.format field StructName.format s_name)
|
2023-03-31 18:56:45 +03:00
|
|
|
fields)
|
2023-03-30 19:52:29 +03:00
|
|
|
p.program_ctx.ctx_structs
|
|
|
|
|
2023-03-31 12:47:44 +03:00
|
|
|
let detect_unused_enum_constructors (p : program) : unit =
|
|
|
|
let enum_constructors_used =
|
|
|
|
Ast.fold_exprs
|
|
|
|
~f:(fun enum_constructors_used e ->
|
|
|
|
let rec enum_constructors_used_expr e enum_constructors_used =
|
2023-05-17 16:44:57 +03:00
|
|
|
match Mark.remove e with
|
2023-03-31 12:47:44 +03:00
|
|
|
| EInj { name = _; e = e_enum; cons } ->
|
|
|
|
EnumConstructor.Set.add cons
|
|
|
|
(enum_constructors_used_expr e_enum enum_constructors_used)
|
|
|
|
| EMatch { e = e_match; name = _; cases } ->
|
|
|
|
let enum_constructors_used =
|
|
|
|
enum_constructors_used_expr e_match enum_constructors_used
|
|
|
|
in
|
|
|
|
EnumConstructor.Map.fold
|
|
|
|
(fun cons e_cons enum_constructors_used ->
|
|
|
|
EnumConstructor.Set.add cons
|
|
|
|
(enum_constructors_used_expr e_cons enum_constructors_used))
|
|
|
|
cases enum_constructors_used
|
|
|
|
| _ ->
|
2023-04-03 14:42:14 +03:00
|
|
|
Expr.shallow_fold enum_constructors_used_expr e
|
|
|
|
enum_constructors_used
|
2023-03-31 12:47:44 +03:00
|
|
|
in
|
|
|
|
enum_constructors_used_expr e enum_constructors_used)
|
|
|
|
~init:EnumConstructor.Set.empty p
|
|
|
|
in
|
|
|
|
EnumName.Map.iter
|
2023-08-10 17:52:39 +03:00
|
|
|
(fun e_name (path, constructors) ->
|
|
|
|
if path <> [] then ()
|
|
|
|
else if
|
2023-03-31 14:50:02 +03:00
|
|
|
EnumConstructor.Map.for_all
|
|
|
|
(fun cons _ ->
|
|
|
|
not (EnumConstructor.Set.mem cons enum_constructors_used))
|
|
|
|
constructors
|
|
|
|
then
|
2023-06-13 12:27:45 +03:00
|
|
|
Message.emit_spanned_warning
|
2023-03-31 14:50:02 +03:00
|
|
|
(snd (EnumName.get_info e_name))
|
2023-06-07 19:15:14 +03:00
|
|
|
"The enumeration @{<yellow>\"%a\"@} is never used; maybe it's \
|
|
|
|
unnecessary?"
|
2023-07-12 12:48:46 +03:00
|
|
|
EnumName.format e_name
|
2023-03-31 14:50:02 +03:00
|
|
|
else
|
|
|
|
EnumConstructor.Map.iter
|
|
|
|
(fun constructor _ ->
|
|
|
|
if not (EnumConstructor.Set.mem constructor enum_constructors_used)
|
|
|
|
then
|
2023-06-13 12:27:45 +03:00
|
|
|
Message.emit_spanned_warning
|
2023-03-31 14:50:02 +03:00
|
|
|
(snd (EnumConstructor.get_info constructor))
|
2023-06-07 19:15:14 +03:00
|
|
|
"The constructor @{<yellow>\"%a\"@} of enumeration \
|
|
|
|
@{<yellow>\"%a\"@} is never used; maybe it's unnecessary?"
|
2023-07-12 12:48:46 +03:00
|
|
|
EnumConstructor.format constructor EnumName.format e_name)
|
2023-03-31 14:50:02 +03:00
|
|
|
constructors)
|
2023-03-31 12:47:44 +03:00
|
|
|
p.program_ctx.ctx_enums
|
|
|
|
|
2023-06-03 19:02:57 +03:00
|
|
|
(* Reachability in a graph can be implemented as a simple fixpoint analysis with
|
|
|
|
backwards propagation. *)
|
|
|
|
module Reachability =
|
|
|
|
Graph.Fixpoint.Make
|
|
|
|
(Dependency.ScopeDependencies)
|
|
|
|
(struct
|
|
|
|
type vertex = Dependency.ScopeDependencies.vertex
|
|
|
|
type edge = Dependency.ScopeDependencies.E.t
|
|
|
|
type g = Dependency.ScopeDependencies.t
|
|
|
|
type data = bool
|
|
|
|
|
|
|
|
let direction = Graph.Fixpoint.Backward
|
|
|
|
let equal = ( = )
|
|
|
|
let join = ( || )
|
|
|
|
let analyze _ x = x
|
|
|
|
end)
|
|
|
|
|
|
|
|
let detect_dead_code (p : program) : unit =
|
|
|
|
(* Dead code detection for scope variables based on an intra-scope dependency
|
|
|
|
analysis. *)
|
|
|
|
ScopeName.Map.iter
|
|
|
|
(fun scope_name scope ->
|
|
|
|
let scope_dependencies = Dependency.build_scope_dependencies scope in
|
|
|
|
let is_alive (v : Dependency.ScopeDependencies.vertex) =
|
|
|
|
match v with
|
|
|
|
| Assertion _ -> true
|
|
|
|
| SubScope _ -> true
|
|
|
|
| Var (var, state) ->
|
|
|
|
let scope_def =
|
|
|
|
ScopeDef.Map.find (Var (var, state)) scope.scope_defs
|
|
|
|
in
|
|
|
|
Mark.remove scope_def.scope_def_io.io_output
|
|
|
|
(* A variable is initially alive if it is an output*)
|
|
|
|
in
|
|
|
|
let is_alive = Reachability.analyze is_alive scope_dependencies in
|
|
|
|
ScopeVar.Map.iter
|
|
|
|
(fun var states ->
|
|
|
|
let emit_unused_warning () =
|
2023-06-13 12:27:45 +03:00
|
|
|
Message.emit_spanned_warning
|
2023-06-03 19:02:57 +03:00
|
|
|
(Mark.get (ScopeVar.get_info var))
|
|
|
|
"This variable is dead code; it does not contribute to computing \
|
2023-06-07 19:15:14 +03:00
|
|
|
any of scope @{<yellow>\"%s\"@} outputs. Did you forget \
|
|
|
|
something?"
|
2023-06-07 19:10:50 +03:00
|
|
|
(Mark.remove (ScopeName.get_info scope_name))
|
2023-06-03 19:02:57 +03:00
|
|
|
in
|
|
|
|
match states with
|
|
|
|
| WholeVar ->
|
|
|
|
if not (is_alive (Var (var, None))) then emit_unused_warning ()
|
|
|
|
| States states ->
|
|
|
|
List.iter
|
|
|
|
(fun state ->
|
|
|
|
if not (is_alive (Var (var, Some state))) then
|
|
|
|
emit_unused_warning ())
|
|
|
|
states)
|
|
|
|
scope.scope_vars)
|
|
|
|
p.program_scopes
|
|
|
|
|
2023-03-30 19:52:29 +03:00
|
|
|
let lint_program (p : program) : unit =
|
|
|
|
detect_empty_definitions p;
|
2023-06-03 19:02:57 +03:00
|
|
|
detect_dead_code p;
|
2023-03-31 12:47:44 +03:00
|
|
|
detect_unused_struct_fields p;
|
2023-04-14 18:38:10 +03:00
|
|
|
detect_unused_enum_constructors p;
|
|
|
|
detect_identical_rules p
|