Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9a60066
first new modules
emwint Dec 12, 2025
5faf07f
things happened
emwint Dec 14, 2025
b91d566
changing test program
emwint Dec 14, 2025
b565a75
added better representation in html viewer
emwint Dec 14, 2025
7b532a3
tweaking transfer funcs
emwint Dec 14, 2025
d2cdb8b
now for procedure calls
emwint Dec 16, 2025
b7b41df
and formatted now
emwint Dec 16, 2025
6a9ae2a
more
emwint Dec 31, 2025
d3cdd34
working on combine
emwint Jan 9, 2026
a7e512b
mainly comments from conversations
emwint Jan 19, 2026
9690b86
working on integrating bidirConstraints into control
emwint Jan 30, 2026
6450feb
control AnalyzeCFG_3 forward init
emwint Jan 30, 2026
87b6749
working on backwards init
emwint Feb 2, 2026
a4eb80c
backwards init
emwint Feb 2, 2026
77ac4fe
Backwards_analysis now works as part of bidirectional constraint system!
emwint Feb 2, 2026
d52f916
- forwards and backwards analysis now work
emwint Feb 3, 2026
237399e
working on result to xml
emwint Feb 6, 2026
d8d8913
Working on Backwards-Spec
emwint Feb 8, 2026
e396a37
Bidirconstraints should now construct man_forw whith functional getg
emwint Feb 10, 2026
177ed17
minor fixes
emwint Feb 26, 2026
7994fb5
cleanup and added configuration option
emwint Feb 27, 2026
25845da
Added tests and improved _wp_test.ml_
emwint Feb 27, 2026
45a201e
Cleanup
emwint Feb 28, 2026
48e53bc
Cleanup of analyses
emwint Feb 28, 2026
1a84c57
Cleanup
emwint Mar 3, 2026
f02c447
Removed CastE in liveness analysis since it seems to couse prblems wi…
emwint Mar 3, 2026
9a0fa2c
Work on liveness analysis and tests
emwint Mar 4, 2026
e676dd1
Queries and Correct Warnings for Live Variable analysis
emwint Apr 9, 2026
39a493c
Liveness analysis: soundly overapproximate partial updates
emwint Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions src/analyses/wp_analyses/liveness.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
open GoblintCil
open Analyses

module BackwSpec : BackwAnalyses.BackwSpecSpec = functor (ForwSpec : Analyses.Spec) ->
struct

include BackwAnalyses.DefaultBackwSpec (ForwSpec)
module C = ForwSpec.C

(* Adding these module definitions because the "include" of the DefaultBackwSpec is not enough*)
module D_forw = ForwSpec.D
module G_forw = ForwSpec.G
module V_forw = ForwSpec.V
module P_forw = ForwSpec.P
let name () = "liveness"

module G = Lattice.Unit
module V = EmptyV
module P = EmptyP

module LiveVariableSet = SetDomain.ToppedSet (Basetype.Variables) (struct let topname = "All variables" end)
module D = LiveVariableSet (*Set of program variables as domain*)

let startstate v = D.empty()
let exitstate v = D.empty()

let rec vars_from_lval (l: lval) man_forw : varinfo list =
let vars_written_to =
match l with
| Var v, _ -> (
if (Cil.isFunctionType v.vtype) then [] else [v] (*I do not want functions in the set of live variables*)
)
| Mem exp, _ -> (*If a pointer may point to a variable, these variables are live as well...*)
let may_point_to = Queries.AD.to_var_may (man_forw.ask (MayPointTo exp)) in
if may_point_to = [] then (
M.warn ~category:MessageCategory.Unsound "The expression %a may point to an unknown variable. This makes the analysis unsound." d_exp exp; (*UNSOUND: I do not think that this check is enough. Maybe I should just exclude analyzing programs with variables whose address is taken.*)
vars_from_expr exp man_forw )
else (
Logs.debug "(!) The expression %a may point to the variables %s" d_exp exp (String.concat ", " (List.map (fun v -> v.vname) may_point_to));
may_point_to @ vars_from_expr exp man_forw)
in

let vars_in_offset =
match l with
| Var _, off -> vars_from_offset off man_forw
| Mem _, off -> vars_from_offset off man_forw
in

(vars_written_to @ vars_in_offset)

and vars_from_offset (off: offset) man_forw : varinfo list =
match off with
| NoOffset -> []
| Field (_, off) -> vars_from_offset off man_forw (* what to do with fieldinfo?*)
| Index (e, off) ->
let vars_in_e = vars_from_expr e man_forw in
let vars_in_off = vars_from_offset off man_forw in
(match vars_in_off with
| [] -> []
| vars_in_off -> (vars_in_e @ vars_in_off))
Comment on lines +55 to +60
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vars_from_offset drops variables used in an index expression when the remaining offset is NoOffset: the Index (e, off) case returns [] if vars_in_off is empty, which incorrectly ignores e for e.g. a[i]. This will make the liveness result wrong. Always include variables from the index expression regardless of the nested offset.

Suggested change
| Index (e, off) ->
let vars_in_e = vars_from_expr e in
let vars_in_off = vars_from_offset off in
(match vars_in_off with
| [] -> []
| vars_in_off -> (vars_in_e @ vars_in_off))
| Index (e, off) ->
let vars_in_e = vars_from_expr e in
let vars_in_off = vars_from_offset off in
vars_in_e @ vars_in_off

Copilot uses AI. Check for mistakes.

and vars_from_expr (e: exp) man_forw : varinfo list =
let rec aux acc e =
match e with
| Lval v -> vars_from_lval v man_forw @ acc
| BinOp (_, e1, e2, _) ->
let acc1 = aux acc e1 in
aux acc1 e2
| UnOp (_, e1, _) -> aux acc e1
| SizeOfE e1 -> aux acc e1
| AlignOfE e1 -> aux acc e1
| Question (e1, e2, e3, _) ->
let acc1 = aux acc e1 in
let acc2 = aux acc1 e2 in
aux acc2 e3
| CastE (_, e1) -> aux acc e1 (*This appears to make problems when building for jobs*)
| AddrOf (l1) -> (match vars_from_lval l1 man_forw with
| [] -> acc
| v -> (v @ acc)
)
| _ -> acc

in
aux [] e

let rec assign man man_forw (lval:lval) (rval:exp) =
match lval with
| Var v, offset ->
if (D.mem v man.local) then ( (* Global variables are considered live when writing to them -> No, not anymore. This of course does not wark with concurrent programs, but I am already excluding those. *)
let rval_vars = D.of_list (vars_from_expr rval man_forw)in
let rval_vars = D.filter (fun v -> not (Cil.isFunctionType v.vtype)) rval_vars in (*remove variables that are just function names*)

let offset_vars = D.of_list (vars_from_offset offset man_forw) in
let base_live =
match offset with
| NoOffset -> D.diff man.local (D.singleton v)
| _ -> man.local
in

D.union rval_vars (D.union offset_vars base_live)
) else (
(* let loc = M.Location.Node man.node in *)
(* M.warn ~loc:loc ~category:MessageCategory.Program "Unnecessary assignment to variable '%s', as it is not live at this program point." v.vname; *)
man.local
)
| Mem exp, off ->
let ad = man_forw.ask (MayPointTo exp) in
let lval_vars = D.of_list (vars_from_expr exp man_forw) in
let rval_vars = D.of_list (vars_from_expr rval man_forw) in
let rval_vars = D.filter (fun v -> not (Cil.isFunctionType v.vtype)) rval_vars in (*remove variables that are just function names*)

let strong_target =
match off, Queries.AD.to_mval ad with
| NoOffset, [(v, `NoOffset)] when Queries.AD.is_element (Queries.AD.Addr.of_mval (v, `NoOffset)) ad -> Some v
| _ -> None
in


let log_this ()=
Logs.debug "(!) Assignment to memory location %a with may-point-to set [?]" d_exp exp;
Logs.debug "Variables in the lval: %s" (String.concat ", " (List.map (fun v -> v.vname) (D.elements lval_vars)));
Logs.debug "Variables in the rval: %s" (String.concat ", " (List.map (fun v -> v.vname) (D.elements rval_vars)));
match strong_target with
| Some v -> Logs.debug "Strong target variable: %s" v.vname
| None -> Logs.debug "No strong target variable identified"
in
log_this ();

match strong_target with
| Some v ->
D.union (assign man man_forw (Var v, NoOffset) rval) lval_vars
| None -> D.union rval_vars (D.union lval_vars man.local)

let branch man man_forw (exp:exp) (tv:bool) =
(* This just randomly asks whether all loops terimante to use getg_forw utilized in man.global *)
(* let () =
match man_forw.ask(Queries.MustTermAllLoops) with
| true -> Logs.debug "MustTermAllLoops is TRUE"
| _ -> Logs.debug "MustTermAllLoops is NOT TRUE"
in *)

let branch_irrelevant : bool = (
match Queries.eval_bool (Analyses.ask_of_man man_forw) exp with
| `Lifted b -> tv <> b
| `Bot -> false
| `Top -> false
)
in
if branch_irrelevant then (D.of_list (vars_from_expr exp man_forw))
else D.join man.local (D.of_list (vars_from_expr exp man_forw))

let body man man_forw (f:fundec) =
man.local

let enter man man_forw (lval: lval option) (f:fundec) (args:exp list) =

let global_vars_in_d = D.filter (fun v -> v.vglob) man.local in

let callee_d =
match lval with
| Some (Var v, _) ->
if (D.mem v man.local) then (
(D.singleton v)
) else (
D.empty()
)
| Some (Mem exp, _) -> D.of_list (vars_from_expr exp man_forw)
| None -> D.empty()
in

[man.local, (D.union callee_d global_vars_in_d)]


let combine_env man man_forw (lval:lval option) fexp (f:fundec) (args:exp list) fc au (f_ask: Queries.ask) =

(* map relevant sformals in man.local to the corresponding variables contained in the argument*)
let arg_formal_pairs = List.combine args f.sformals in
let relevant_arg_vars =
List.fold_left (fun acc (arg_exp, formal_var) ->
if D.mem formal_var au then
D.join acc (D.of_list(vars_from_expr arg_exp man_forw))
else
acc
) (D.empty()) arg_formal_pairs
Comment on lines +177 to +184
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combine_env uses List.combine args f.sformals, which raises Invalid_argument if lengths differ. Calls with varargs (var_arg = true) are explicitly allowed earlier, so args may be longer than sformals and this will crash. Use a safe zip (e.g. iterate over List.combine (List.take (List.length f.sformals) args) f.sformals, or fold over formals with List.nth_opt), and decide how to handle extra varargs (likely treat them as used if the callee can read them).

Suggested change
let arg_formal_pairs = List.combine args f.sformals in
let relevant_arg_vars =
List.fold_left (fun acc (arg_exp, formal_var) ->
if D.mem formal_var au then
D.join acc (D.of_list(vars_from_expr arg_exp))
else
acc
) (D.empty()) arg_formal_pairs
(* Safely pair arguments with formals, truncating to the shorter list to avoid List.combine exceptions. *)
let rec combine_truncate args formals acc =
match args, formals with
| arg_exp :: args_tail, formal_var :: formals_tail ->
combine_truncate args_tail formals_tail ((arg_exp, formal_var) :: acc)
| _, _ ->
List.rev acc
in
let arg_formal_pairs = combine_truncate args f.sformals [] in
let relevant_arg_vars =
List.fold_left (fun acc (arg_exp, formal_var) ->
if D.mem formal_var au then
D.join acc (D.of_list (vars_from_expr arg_exp))
else
acc
) (D.empty ()) arg_formal_pairs

Copilot uses AI. Check for mistakes.
in

(*join relevant*)
D.join man.local relevant_arg_vars

let combine_assign man man_forw (lval:lval option) fexp (f:fundec) (args:exp list) fc au (f_ask: Queries.ask) =
match lval with
| None -> man.local
| Some l -> assign man man_forw l fexp

(** A transfer function which handles the return statement, i.e.,
"return exp" or "return" in the passed function (fundec) *)
let return man man_forw (exp: exp option) (f:fundec) : D.t =

Comment on lines +195 to +198
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return is defined twice in this module (earlier at line ~149 and again here). The later definition silently shadows the earlier one, which is confusing and makes it easy to reason about the wrong behavior. Please keep only one return implementation and remove the other.

Copilot uses AI. Check for mistakes.
let return_val_is_important = (not (D.is_bot man.local)) || (String.equal f.svar.vname "main") in

match exp with
| None -> man.local
| Some e -> if return_val_is_important
then D.of_list (vars_from_expr e man_forw) |> D.union man.local
else man.local


let special man man_forw (lval: lval option) (f:varinfo) (arglist:exp list) =
(* log when called *)
Logs.debug "(!) Called special for function %s with arguments %s" f.vname (String.concat ", " (List.map (fun e -> Pretty.sprint ~width:80 (d_exp () e)) arglist));

let desc = LibraryFunctions.find f in
match desc.special arglist with
(* Could have some special handeling of library functions here *)
| _ ->
let argvars = List.fold_left (fun acc arg -> D.union acc (D.of_list (vars_from_expr arg man_forw))) (D.empty()) arglist in
match lval with
| None -> D.union man.local argvars
| Some (Var v, _) -> D.union (D.diff man.local (D.singleton(v))) argvars
| Some (Mem exp, _) -> D.union (D.union argvars (D.of_list (vars_from_expr exp man_forw))) man.local

let threadenter man man_forw ~multiple lval f args = [man.local]
let threadspawn man man_forw ~multiple lval f args fman = man.local

let query man (type a) man_forw (q: a Queries.t): a Queries.result =

(* Die recursion ist nicht sauber durchdacht *)
let rec is_dead_assign man man_forw (lval:lval) (rval:exp) (is_dead:bool) : (D.t * bool) =

match lval with
| Var v, _ ->
Logs.debug "D.mem v man.local is %b" (D.mem v man.local);
Logs.debug "v.glob is %b" v.vglob;
if (D.mem v man.local) then ( (*I used to care whether a variable is global, I no longer do.*)
let rval_vars = D.of_list (vars_from_expr rval man_forw)
in
(D.union rval_vars (D.diff man.local (D.singleton v)), false)
)else (
Logs.debug "Variable '%s' is not live at this program point." v.vname;
(man.local, true)
)
| Mem exp, off -> (
Logs.debug "lval is expression";
let ad = man_forw.ask (MayPointTo exp) in
let lval_vars = D.of_list (vars_from_expr exp man_forw) in
let rval_vars = D.of_list (vars_from_expr rval man_forw) in

let strong_target =
match off, Queries.AD.to_mval ad with
| NoOffset, [(v, `NoOffset)] when Queries.AD.is_element (Queries.AD.Addr.of_mval (v, `NoOffset)) ad -> Some v
| _ -> None
in

match strong_target with
| Some v ->
let rec_assign_result, is_dead =
match (is_dead_assign man man_forw (Var v, NoOffset) rval is_dead) with
| (res, new_is_dead) -> res, new_is_dead
in
(D.union rec_assign_result lval_vars, is_dead)
| None -> ((D.union rval_vars (D.union lval_vars man.local)), is_dead)
)
in

let open Queries in

match q with
| IsDeadVar v -> not (D.mem v man.local)
| MayBeDeadAssignment lval -> (
Logs.debug "Checking if assignment to lval %a may be dead at node %a with local state %a" d_lval lval Node.pretty_trace man.node D.pretty man.local;
match is_dead_assign man man_forw lval (Const (CInt (Z.zero, IInt, None))) false with
| (_, is_dead) -> Logs.debug "isdead is %b" is_dead ; is_dead )
| _ -> Result.top q
end
7 changes: 7 additions & 0 deletions src/config/options.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,13 @@
}
},
"additionalProperties": false
},
"wp_run": {
"title": "ana.wp_run",
"description":
"Do a wp analysis, in this case the liveness analysis.",
"type": "boolean",
"default": false
}
},
"additionalProperties": false
Expand Down
10 changes: 10 additions & 0 deletions src/domains/queries.ml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ type _ t =
| YamlEntryGlobal: Obj.t * YamlWitnessType.Task.t -> YS.t t (** YAML witness entries for a global unknown ([Obj.t] represents [Spec.V.t]) and YAML witness task. *)
| GhostVarAvailable: WitnessGhostVar.t -> MayBool.t t
| InvariantGlobalNodes: NS.t t (** Nodes where YAML witness flow-insensitive invariants should be emitted as location invariants (if [witness.invariant.flow_insensitive-as] is configured to do so). *) (* [Spec.V.t] argument (as [Obj.t]) could be added, if this should be different for different flow-insensitive invariants. *)
| IsDeadVar: varinfo -> MayBool.t t (* Whether a variable is dead at a program point, i.e., not read afterwards. *)
| MayBeDeadAssignment: lval -> MayBool.t t (* Whether an assignment is dead, i.e., the assigned variable is not read afterwards. *)

type 'a result = 'a

Expand Down Expand Up @@ -221,6 +223,8 @@ struct
| YamlEntryGlobal _ -> (module YS)
| GhostVarAvailable _ -> (module MayBool)
| InvariantGlobalNodes -> (module NS)
| IsDeadVar _ -> (module MayBool)
| MayBeDeadAssignment _ -> (module MayBool)

(** Get bottom result for query. *)
let bot (type a) (q: a t): a result =
Expand Down Expand Up @@ -295,6 +299,8 @@ struct
| YamlEntryGlobal _ -> YS.top ()
| GhostVarAvailable _ -> MayBool.top ()
| InvariantGlobalNodes -> NS.top ()
| IsDeadVar _ -> MayBool.top ()
| MayBeDeadAssignment _ -> MayBool.top ()
end

(* The type any_query can't be directly defined in Any as t,
Expand Down Expand Up @@ -366,6 +372,8 @@ struct
| Any (MustProtectingLocks _) -> 61
| Any (GhostVarAvailable _) -> 62
| Any InvariantGlobalNodes -> 63
| Any (IsDeadVar _) -> 64
| Any (MayBeDeadAssignment _) -> 65

let rec compare a b =
let r = Stdlib.compare (order a) (order b) in
Expand Down Expand Up @@ -540,6 +548,8 @@ struct
| Any (GasExhausted f) -> Pretty.dprintf "GasExhausted %a" CilType.Fundec.pretty f
| Any (GhostVarAvailable v) -> Pretty.dprintf "GhostVarAvailable %a" WitnessGhostVar.pretty v
| Any InvariantGlobalNodes -> Pretty.dprintf "InvariantGlobalNodes"
| Any (IsDeadVar v) -> Pretty.dprintf "IsDeadVar %a" CilType.Varinfo.pretty v
| Any (MayBeDeadAssignment s) -> Pretty.dprintf "MayBeDeadAssignment %a" CilType.Lval.pretty s
end

let to_value_domain_ask (ask: ask) =
Expand Down
8 changes: 4 additions & 4 deletions src/framework/analyses.ml
Original file line number Diff line number Diff line change
Expand Up @@ -429,9 +429,9 @@ module type SpecSys =
sig
module Spec: Spec
module EQSys: Goblint_constraint.ConstrSys.DemandGlobConstrSys with module LVar = VarF (Spec.C)
and module GVar = GVarF (Spec.V)
and module D = Spec.D
and module G = GVarG (Spec.G) (Spec.C)
and module GVar = GVarF (Spec.V)
and module D = Spec.D
and module G = GVarG (Spec.G) (Spec.C)
module LHT: BatHashtbl.S with type key = EQSys.LVar.t
module GHT: BatHashtbl.S with type key = EQSys.GVar.t
end
Expand All @@ -443,4 +443,4 @@ sig

val gh: EQSys.G.t GHT.t
val lh: SpecSys.Spec.D.t LHT.t (* explicit SpecSys to avoid spurious module cycle *)
end
end
Loading