Skip to content

Latest commit

 

History

History
268 lines (209 loc) · 9.9 KB

Debugging.md

File metadata and controls

268 lines (209 loc) · 9.9 KB

Debugging a tactic in HOL Light

This document introduces methods to debug a tactic in HOL Light.

1. Debugging tools of OCaml

In toplevel, you can use Printexc.record_backtrace true;; to print a call stack when an exception happened. Note note that the printed call stack may omit some invocations due to the optimizations performed by OCaml. Also, enabling this option will make the speed of tactic significantly slow.

#trace <function name>;; tells the values of arguments passed to the function and its return values. This may give some hints about how the tactic works. You can disable the trace using #untrace <function name>;; or simply #untrace_all. This document has more details.

OCaml also has a C-style printer function Printf.printf "..(format).." ..(args).. (API), which will be familiar if C was one of your languages. Sometimes Printf.printf may not immediately print the string, and this can be resolved by adding %! to the format string (example).

Timing of printing. Let's assume that you want to use Printf.printf to figure out which tactic is failing. It will be tempting to write

let my_proof = prove(`...`,
	SOME_TACTIC1 THEN
	let _ = Printf.printf "SOME_TACTIC1 was fine\n" in
	SOME_TACTIC2 THEN
	let _ = Printf.printf "SOME_TACTIC2 was fine\n" in
	...);;

but this won't work because it will always print the whole messages successfully. This is because function arguments are always eagerly evaluated in OCaml. So, messages will have been already printed before the prove function is invoked.

In this case, Printf.printf must be wrapped inside a function which will run when prove is eventually started. Or, ... ORELSE FAIL_TAC "message" can be used instead. This will be explained in the later section of this document.

2. Printing terms, types and theorems

There are string_of_* functions that convert a HOL Light object into OCaml string: string_of_term, string_of_type and string_of_thm. You can use Printf.printf "..(format).." ..(args).. to print them. There are also shorter versions print_term, print_type and print_thm. You can also use pp_print_* and Format.asprintf:

Format.asprintf "test: %a %a" pp_print_qterm `1` pp_print_qterm `1+2`;;

Printing types of subterms. By default, HOL Light will not print the types of subterms unless they are invented type variables like :?123. To force HOL Light to print every subtype, set print_types_of_subterms := 2;;. Its help doc has nice examples about this.

Printing the actual OCaml data structure. Sometimes it is necessary to understand the actual OCaml data structure that represents a term when e.g., writing a function matching a term. In this case, removing the printer will give the information.

#remove_printer pp_print_qterm;;
# `1 + x`;;
val it : term =
  Comb
   (Comb (Const ("+", `:num->num->num`),
     Comb (Const ("NUMERAL", `:num->num`),
      Comb (Const ("BIT1", `:num->num`), Const ("_0", `:num`)))),
   Var ("x", `:num`))
# #install_printer pp_print_qterm;;
# `1 + x`;;
val it : term = `1 + x`

3. Printing the goal state

There is PRINT_GOAL_TAC (doc) which is a tactic that simply prints the goal state. You can combine this with ORELSE so that the goal state is printed and a failure is returned if the tactic fails:

(... (a tactic that can fail) ...) ORELSE (PRINT_GOAL_TAC THEN FAIL_TAC "my message")

If you want to print more messages, you can write your own tactic that looks like this:

let MY_PRINT_CONCLUSION_TAC:tactic =
  fun (asl,g) ->
    let _ = Printf.printf "The conclusion was: `%s`\n" (string_of_term g) in
    ALL_TAC (asl,g);;

If the goal is too large to print in a reasonable terminal buffer size, you can set print_goal_hyp_max_boxes := Some <small number>;; (doc). This will make the goal printer omit subexpressions with dots if they use more 'boxes' than the parameter.

If you are editing your proof using g() and e() (the subgoal package), other than using e(PRINT_GOAL_TAC) you can also use:

# print_goal (top_realgoal());;

4. Asserting that the current goal state is in a good shape.

Predicates for checking a term

You can use term_match (doc) to assert that the conclusion is in a good shape.

let MY_CHECKER_TAC:tactic =
  fun (asl,g) ->
    (* Let's assert that the conclusion has the form 'x = y'. *)
    let _ = term_match [] `x = y` g in
    ALL_TAC (asl,g);;
# g `1 < 2`;;
val it : goalstack = 1 subgoal (1 total)

`1 < 2`

# e(MY_CHECKER_TAC);; (* This should fail *)
Warning: inventing type variables
Exception: Failure "term_pmatch".

To check that two terms are alpha-equivalent, aconv (doc) can be used.

# aconv `?x. x <=> T` `?y. y <=> T`;;
val it : bool = true

Getting free variables. To check a variable appears as a free variable inside an expression, you can use vfree_in (doc). frees return a list of free variables (doc).

# vfree_in `x:num` `x + y + 1`;;
val it : bool = true
# frees `x = 1 /\ y = 2 /\ !z. z >= 0`;;
val it : term list = [`x`; `y`]

Decomposing a term.

  • Numeral: You can use is_numeral to check whether the term is a constant numeral (doc), and use dest_numeral (doc) to get the actual constant. The returned constant is a general-precision number datatype num. If you know that the constant should fit in OCaml int, use dest_small_number(doc).

  • Function application: strip_comb will break f x y z into (`f`,[`x`;`y`;`z`]) (doc).

  • Others: there are is_forall, strip_forall, is_conj, dest_conj, conjuncts, etc.

Checking that tactic actually did something

CHANGED_TAC t is a tactic that fails if tactic t did not change the goal state.

Dealing with subgoals

To check that there only is one subgoal, tactic THENL [ ALL_TAC ] THEN next_tactic can be used. To check that there exactly are n subgoals, tactic THENL (map (fun _ -> ALL_TAC) (1--10)) THEN next_tactic will work.

Which Exception type should I use?

The Failure exception type is commonly used in HOL Light to stop execution if an unexpected situation happened. This can be conveniently raised usihg the failwith "msg" function of OCaml, which is equivalent to raise (Failure "msg"). However, there are several predefined types of exceptions in OCaml, and Failure is only one of them. For example, assert will raise Assert_failure which is slightly different from Failure. My suggestion is to stick to Failure in HOL Light unless it is very necessary to use the other types. For example, t1 ORELSE t2 will execute t2 if t1 raised Failure, or immediately stop otherwise. This link has more info for generic situations.

5. Other useful tricks

Tips for understanding a complex tactic.

If a tactic starts with fun (asl,g) -> ..., one easy start is to initiate asl and g using the following command:

let asl,g = top_realgoal();;

and follow the statements step by step. Note that this overwrites the global definition of g which is used to set the goal (e.g., "g 1 + 1 = 2;;"). The original definition can be recovered by

let g t = set_goal([],t);;

If it contains a complicated expression, a subexpression can be explored by typing it at toplevel and using the returned it variable to explore an expression that subsumes it.

Flags for immediately stopping at failures or warnings.

Nested file load failure. HOL Light does not stop by default when a failure happend during loading nested files. To control this behavior, setting use_file_raise_failure := true;; will work (doc).

Avoiding invented types. Invented types can be problematic in some tactics like SUBGOAL_THEN which will a bogus, unprovable goal. To immediately stop when types are invented, type_invention_error := true;; will work (doc)

Understanding why a tactic is slow.

In general, a tactic can be slow for various reasons.

  • Nested DEPTH_CONV and REWRITE_CONV: REWRITE_CONV recursively visits the subterms. If it is used inside DEPTH_CONV, this can cause a significant slowdown because the subterms are recursively visited twice.

  • Use of prove(): If the tactic proves a custom lemma using prove() on the fly, the prove() invocation can be a bottleneck because it is slower. One possible solution is to cache the lemmas that are proven by prove() and reuse it.

  • Validity check of e(): Sometimes the speed of e() can significantly slow down due to its validity check. This can be temporarily avoided by redefining e as:

(*let e tac = refine(by(VALID tac));;*)
let e tac = refine(by(tac));;

TODO: use of CLARIFY_TAC in s2n-bignum

Omitting proof checks of needed .ml files for speed

It is often not necessary to check the proofs of the proof files that is being loaded. needs_skip_proofs is a tactic that can replace needs to omit the prove() calls inside the file.