Skip to content

Commit 8b14d47

Browse files
committed
Add native Python coqchk support to find_bug.py
- Add get_coqchk_help and get_coqchk_version functions to coq_version.py - Add coqchk_prog and coqchk_prog_args parameters to get_coq_output in diagnose_error.py - Process coqchk output with Fatal Error handling - Add --coqchk-args, --nonpassing-coqchk-args, --passing-coqchk-args CLI options - Set coqchk/passing_coqchk to None when --chk is not specified - Pass coqchk_prog and coqchk_prog_args explicitly to all get_coq_output calls - Extend argument processing loops to handle coqchk args similar to coqtop
1 parent df4c970 commit 8b14d47

File tree

3 files changed

+202
-23
lines changed

3 files changed

+202
-23
lines changed

coq_tools/custom_arguments.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ def argstring_to_iterable(arg):
413413

414414

415415
def append_coq_arg(env, arg, passing=""):
416-
for key in ("coqc_args", "coqtop_args"):
416+
for key in ("coqc_args", "coqtop_args", "coqchk_args"):
417417
env[passing + key] = tuple(
418418
list(env.get(passing + key, [])) + list(argstring_to_iterable(arg))
419419
)

coq_tools/diagnose_error.py

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
from .file_util import clean_v_file
66
from .util import re_escape, get_peak_rss_bytes, apply_memory_limit, cleanup_cgroup
77
from .custom_arguments import LOG_ALWAYS
8-
from .coq_version import group_coq_args, get_coqc_help, get_coq_accepts_Q
8+
from .coq_version import (
9+
group_coq_args,
10+
get_coqc_help,
11+
get_coq_accepts_Q,
12+
)
913
from . import util
1014

1115
__all__ = [
@@ -42,6 +46,26 @@ def clean_output(output):
4246
return adjust_error_message_for_selected_errors(util.normalize_newlines(output))
4347

4448

49+
def process_coqchk_output(output, v_file_name, line_count):
50+
"""
51+
Process coqchk output and insert error preamble before "Fatal Error:" lines.
52+
53+
When coqchk outputs a line starting with "Fatal Error:", we emit a fake Coq
54+
error location pointing to the end of the .v file before that line.
55+
"""
56+
lines = output.split("\n")
57+
result_lines = []
58+
for line in lines:
59+
if line.startswith("Fatal Error:"):
60+
# Insert fake Coq error location before the Fatal Error line
61+
result_lines.append(
62+
f'File "{v_file_name}", line {line_count}, characters 0-0:'
63+
)
64+
result_lines.append("Error:")
65+
result_lines.append(line)
66+
return "\n".join(result_lines)
67+
68+
4569
@memoize
4670
def get_coq_accepts_fine_grained_debug(coqc, debug_kind):
4771
temp_file = tempfile.NamedTemporaryFile(suffix=".v", dir=".", delete=True)
@@ -548,8 +572,11 @@ def reset_coq_output_cache(
548572
pass_on_stdin=False,
549573
verbose_base=1,
550574
ocamlpath=None,
575+
coqchk_prog=None,
576+
coqchk_prog_args=(),
551577
**kwargs,
552578
):
579+
key_extra = (coqchk_prog, tuple(coqchk_prog_args)) if coqchk_prog else ()
553580
key, file_name, cmds, input_val, cleaner, env = prepare_cmds_for_coq_output(
554581
coqc_prog,
555582
coqc_prog_args,
@@ -562,6 +589,7 @@ def reset_coq_output_cache(
562589
ocamlpath=ocamlpath,
563590
**kwargs,
564591
)
592+
key = key + key_extra
565593
cleaner()
566594

567595
if key in COQ_OUTPUT.keys():
@@ -581,10 +609,18 @@ def get_coq_output(
581609
lambda output: "is not a compiled interface for this version of OCaml" in output
582610
),
583611
ocamlpath=None,
612+
coqchk_prog=None,
613+
coqchk_prog_args=(),
584614
**kwargs,
585615
):
586616
"""Returns the coqc output of running through the given
587-
contents. Pass timeout_val = None for no timeout."""
617+
contents. Pass timeout_val = None for no timeout.
618+
619+
If coqchk_prog is provided, after coqc succeeds, coqchk will be run on the
620+
resulting .vo file. The output is combined, but if coqc fails, we end early.
621+
When coqchk outputs "Fatal Error:", a fake Coq error location pointing to
622+
the end of the .v file is inserted before that line.
623+
"""
588624
if (
589625
timeout_val is not None
590626
and timeout_val < 0
@@ -601,9 +637,12 @@ def get_coq_output(
601637
verbose_base=verbose_base,
602638
retry_with_debug_when=retry_with_debug_when,
603639
ocamlpath=ocamlpath,
640+
coqchk_prog=coqchk_prog,
641+
coqchk_prog_args=coqchk_prog_args,
604642
**kwargs,
605643
)
606644

645+
key_extra = (coqchk_prog, tuple(coqchk_prog_args)) if coqchk_prog else ()
607646
key, file_name, cmds, input_val, cleaner, env = prepare_cmds_for_coq_output(
608647
coqc_prog,
609648
coqc_prog_args,
@@ -617,6 +656,9 @@ def get_coq_output(
617656
**kwargs,
618657
)
619658

659+
# Extend key to include coqchk info
660+
key = key + key_extra
661+
620662
if key in COQ_OUTPUT.keys():
621663
cleaner()
622664
return COQ_OUTPUT[key][1]
@@ -659,10 +701,78 @@ def get_coq_output(
659701
)
660702
if get_timeout(coqc_prog) is None and timeout_val is not None:
661703
set_timeout(coqc_prog, 3 * max((1, int(math.ceil(finish - start)))), **kwargs)
704+
705+
combined_stdout = util.s(stdout)
706+
combined_returncode = returncode
707+
combined_runtime = runtime
708+
combined_peak_rss_kb = peak_rss_kb
709+
710+
# If coqchk is enabled and coqc succeeded, run coqchk
711+
if coqchk_prog is not None and returncode == 0:
712+
# Build coqchk command: coqchk -o -silent <args> <vo_file>
713+
vo_file = os.path.splitext(file_name)[0] + ".vo"
714+
assert isinstance(coqchk_prog, tuple), coqchk_prog
715+
coqchk_cmds = [*coqchk_prog, *coqchk_prog_args, vo_file]
716+
chk_start = time.time()
717+
((chk_stdout, chk_stderr), chk_returncode, chk_peak_rss_bytes) = (
718+
memory_robust_timeout_Popen_communicate(
719+
kwargs["log"],
720+
coqchk_cmds,
721+
stderr=subprocess.STDOUT,
722+
stdout=subprocess.PIPE,
723+
stdin=subprocess.PIPE,
724+
timeout=(
725+
timeout_val if timeout_val is not None and timeout_val > 0 else None
726+
),
727+
cwd=cwd,
728+
env=env,
729+
**extra_kwargs,
730+
)
731+
)
732+
chk_finish = time.time()
733+
chk_runtime = chk_finish - chk_start
734+
chk_peak_rss_kb = chk_peak_rss_bytes / 1024
735+
736+
kwargs["log"](
737+
"\ncoqchk retcode: %d\nstdout:\n%s\n\nstderr:\n%s\nruntime:\n%f\npeak_rss:\n%f kb\n\n"
738+
% (
739+
chk_returncode,
740+
util.s(chk_stdout),
741+
util.s(chk_stderr),
742+
chk_runtime,
743+
chk_peak_rss_kb,
744+
),
745+
level=verbose_base + 1,
746+
)
747+
748+
# Count lines in the .v file for error location
749+
line_count = contents.count("\n") + 1
750+
751+
# Process coqchk output to insert error preamble before "Fatal Error:" lines
752+
processed_chk_output = process_coqchk_output(
753+
util.s(chk_stdout), file_name, line_count
754+
)
755+
756+
# Combine outputs
757+
combined_stdout = combined_stdout + "\n" + processed_chk_output
758+
combined_returncode = (
759+
chk_returncode if chk_returncode != 0 else combined_returncode
760+
)
761+
combined_runtime = runtime + chk_runtime
762+
combined_peak_rss_kb = max(peak_rss_kb, chk_peak_rss_kb)
763+
764+
# Now clean up after coqchk has run (or if coqchk wasn't needed)
662765
cleaner()
766+
663767
COQ_OUTPUT[key] = (
664768
file_name,
665-
(clean_output(util.s(stdout)), tuple(cmds), returncode, runtime, peak_rss_kb),
769+
(
770+
clean_output(combined_stdout),
771+
tuple(cmds),
772+
combined_returncode,
773+
combined_runtime,
774+
combined_peak_rss_kb,
775+
),
666776
)
667777
kwargs["log"](
668778
"Storing result: COQ_OUTPUT[%s]:\n%s" % (repr(key), repr(COQ_OUTPUT[key])),
@@ -684,6 +794,8 @@ def get_coq_output(
684794
verbose_base=verbose_base,
685795
retry_with_debug_when=(lambda output: False),
686796
ocamlpath=ocamlpath,
797+
coqchk_prog=coqchk_prog,
798+
coqchk_prog_args=coqchk_prog_args,
687799
**kwargs,
688800
)
689801
return COQ_OUTPUT[key][1]

coq_tools/find_bug.py

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,30 @@
572572
action="append",
573573
help="The path to the coqchk program for the passing coqc (only used with --chk).",
574574
)
575+
parser.add_argument(
576+
"--coqchk-args",
577+
metavar="ARG",
578+
dest="coqchk_args",
579+
type=str,
580+
action="append",
581+
help='Arguments to pass to coqchk (both passing and nonpassing); e.g., " -silent" (leading and trailing spaces are stripped, only used with --chk)',
582+
)
583+
parser.add_argument(
584+
"--nonpassing-coqchk-args",
585+
metavar="ARG",
586+
dest="nonpassing_coqchk_args",
587+
type=str,
588+
action="append",
589+
help='Arguments to pass to nonpassing coqchk; e.g., " -silent" (leading and trailing spaces are stripped, only used with --chk)',
590+
)
591+
parser.add_argument(
592+
"--passing-coqchk-args",
593+
metavar="ARG",
594+
dest="passing_coqchk_args",
595+
type=str,
596+
action="append",
597+
help='Arguments to pass to passing coqchk; e.g., " -silent" (leading and trailing spaces are stripped, only used with --chk)',
598+
)
575599
parser.add_argument(
576600
"--passing-coqc",
577601
metavar="COQC",
@@ -1072,6 +1096,8 @@ def get_error_reg_string(output_file_name, **kwargs):
10721096
verbose_base=1,
10731097
cwd=kwargs["base_dir"],
10741098
ocamlpath=kwargs["nonpassing_ocamlpath"],
1099+
coqchk_prog=kwargs["coqchk"],
1100+
coqchk_prog_args=kwargs["coqchk_args"],
10751101
**kwargs,
10761102
)
10771103
result = ""
@@ -1313,6 +1339,8 @@ def get_padded_contents():
13131339
is_coqtop=kwargs["coqc_is_coqtop"],
13141340
verbose_base=2,
13151341
ocamlpath=kwargs["nonpassing_ocamlpath"],
1342+
coqchk_prog=kwargs["coqchk"],
1343+
coqchk_prog_args=kwargs["coqchk_args"],
13161344
**kwargs,
13171345
)
13181346
if not should_succeed and diagnose_error.has_error(
@@ -1346,6 +1374,8 @@ def get_padded_contents():
13461374
is_coqtop=kwargs["passing_coqc_is_coqtop"],
13471375
verbose_base=2,
13481376
ocamlpath=kwargs["passing_ocamlpath"],
1377+
coqchk_prog=kwargs["passing_coqchk"],
1378+
coqchk_prog_args=kwargs["passing_coqchk_args"],
13491379
**kwargs,
13501380
)
13511381
if not (
@@ -3413,6 +3443,8 @@ def minimize_file(
34133443
verbose_base=2,
34143444
cwd=env["base_dir"],
34153445
ocamlpath=env["nonpassing_ocamlpath"],
3446+
coqchk_prog=env["coqchk"],
3447+
coqchk_prog_args=env["coqchk_args"],
34163448
**env,
34173449
)
34183450
line_num = diagnose_error.get_error_line_number(output, env["error_reg_string"])
@@ -3809,6 +3841,8 @@ def get_test_output(
38093841
is_coqtop=kwargs["coqc_is_coqtop"],
38103842
verbose_base=2,
38113843
ocamlpath=kwargs["nonpassing_ocamlpath"],
3844+
coqchk_prog=kwargs["coqchk"],
3845+
coqchk_prog_args=kwargs["coqchk_args"],
38123846
**kwargs,
38133847
)
38143848
)
@@ -4215,11 +4249,31 @@ def prepend_coqbin(prog):
42154249
"coq_makefile": prepend_coqbin(args.coq_makefile or "coq_makefile"),
42164250
"coqdep": prepend_coqbin(args.coqdep),
42174251
"chk": args.chk,
4218-
"coqchk": prepend_coqbin(args.coqchk or "coqchk"),
4252+
"coqchk": (prepend_coqbin(args.coqchk or "coqchk") if args.chk else None),
42194253
"passing_coqchk": (
4220-
prepend_coqbin(args.passing_coqchk)
4221-
if args.passing_coqchk
4222-
else prepend_coqbin(args.coqchk or "coqchk")
4254+
(
4255+
prepend_coqbin(args.passing_coqchk)
4256+
if args.passing_coqchk
4257+
else prepend_coqbin(args.coqchk or "coqchk")
4258+
)
4259+
if args.chk
4260+
else None
4261+
),
4262+
"coqchk_args": tuple(
4263+
i.strip()
4264+
for i in (
4265+
list(process_maybe_list(args.nonpassing_coqchk_args, log=args.log))
4266+
+ list(process_maybe_list(args.coqchk_args, log=args.log))
4267+
+ list(process_maybe_list(args.coq_args, log=args.log))
4268+
)
4269+
),
4270+
"passing_coqchk_args": tuple(
4271+
i.strip()
4272+
for i in (
4273+
list(process_maybe_list(args.passing_coqchk_args, log=args.log))
4274+
+ list(process_maybe_list(args.coqchk_args, log=args.log))
4275+
+ list(process_maybe_list(args.coq_args, log=args.log))
4276+
)
42234277
),
42244278
"passing_coqc_args": tuple(
42254279
i.strip()
@@ -4414,17 +4468,6 @@ def make_make_coqc(coqc_prog, **kwargs):
44144468
env["passing_coqc"] = env["coqtop"]
44154469
env["passing_make_coqc"] = make_make_coqc(env["passing_coqc"], **env)
44164470

4417-
if env["chk"]:
4418-
# Wrap coqc with coqchk-as-coqc.sh to also run coqchk after coqc
4419-
coqchk_wrapper = str(util.resource_path("coqchk-as-coqc.sh"))
4420-
env["coqc"] = (coqchk_wrapper, *env["coqc"], *env["coqchk"])
4421-
if env["passing_coqc"] is not None:
4422-
env["passing_coqc"] = (
4423-
coqchk_wrapper,
4424-
*env["passing_coqc"],
4425-
*env["passing_coqchk"],
4426-
)
4427-
44284471
coqc_help = get_coqc_help(get_preferred_passing("coqc", **env), **env)
44294472
coqc_version = get_coqc_version(env["coqc"], **env)
44304473

@@ -4559,6 +4602,17 @@ def make_make_coqc(coqc_prog, **kwargs):
45594602
env["passing_coqtop"] if env["passing_coqtop"] else env["coqtop"],
45604603
"passing_",
45614604
),
4605+
) + (
4606+
(
4607+
("coqchk_args", env["coqchk"], ""),
4608+
(
4609+
"passing_coqchk_args",
4610+
env["passing_coqchk"] if env["passing_coqchk"] else env["coqchk"],
4611+
"passing_",
4612+
),
4613+
)
4614+
if env["chk"]
4615+
else ()
45624616
):
45634617
env[args_name] = tuple(list(env[args_name]) + list(extra_args))
45644618
for dirname, libname in env.get(passing_prefix + "libnames", []):
@@ -4587,14 +4641,28 @@ def make_make_coqc(coqc_prog, **kwargs):
45874641
if arg[0] == "-I":
45884642
env.get(passing_prefix + "ocaml_dirnames", []).append(arg[1])
45894643

4590-
for args_name, coqtop_prog, coqc_prog, passing_prefix in (
4591-
("coqtop_args", env["coqtop"], env["coqc"], ""),
4644+
for coqprog, args_name, coqtop_prog, coqc_prog, passing_prefix in (
4645+
("coqtop", "coqtop_args", env["coqtop"], env["coqc"], ""),
45924646
(
4647+
"coqtop",
45934648
"passing_coqtop_args",
45944649
env["passing_coqtop"] if env["passing_coqtop"] else env["coqtop"],
45954650
env["passing_coqc"] if env["passing_coqc"] else env["coqc"],
45964651
"passing_",
45974652
),
4653+
) + (
4654+
(
4655+
("coqchk", "coqchk_args", env["coqchk"], env["coqc"], ""),
4656+
(
4657+
"passing_coqchk",
4658+
"passing_coqchk_args",
4659+
env["passing_coqchk"] if env["passing_coqchk"] else env["coqchk"],
4660+
env["passing_coqc"] if env["passing_coqc"] else env["coqc"],
4661+
"passing_",
4662+
),
4663+
)
4664+
if env["chk"]
4665+
else ()
45984666
):
45994667
coqc_prog_help = get_coqc_help(coqc_prog, **env)
46004668
coqtop_prog_help = get_coqc_help(coqtop_prog, **env)
@@ -4607,8 +4675,7 @@ def make_make_coqc(coqc_prog, **kwargs):
46074675
)
46084676
if len(coqc_but_not_coqtop_args) > 0:
46094677
env["log"](
4610-
"Warning: skipping arguments to coqtop that are only valid for coqc: %s"
4611-
% repr(coqc_but_not_coqtop_args),
4678+
f"Warning: skipping arguments to {coqprog} that are only valid for coqc: {coqc_but_not_coqtop_args!r}",
46124679
level=2,
46134680
)
46144681
env[args_name] = tuple(

0 commit comments

Comments
 (0)