Skip to content

Commit 5603bf6

Browse files
committed
bullets: focusing operators behind +strict_bullets pragma
When `pragma +strict_bullets` is set, the bullet tokens `-`, `+`, `*` (and their repeated forms `--`, `++`, `**`, ...) become Coq-style focusing operators: each phrase's bullet is checked against a per-proof stack so that a subgoal must be discharged before its sibling is addressed, and so that bullet characters identify nesting levels. The default behavior is unchanged: without the pragma, bullets remain pure decoration, preserving every existing proof script. Repeated bullet characters are emitted by the lexer as single PLUSn/MINUSn/STARn tokens (carrying their literal), which makes them usable both as deeper bullet levels and as user-defined binary operators (`op (--) ...`).
1 parent b09b7ae commit 5603bf6

11 files changed

Lines changed: 391 additions & 24 deletions

src/ecCommands.ml

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ type pragma = {
1515
pm_g_prall : bool; (* true => display all open goals *)
1616
pm_g_prpo : EcPrinting.prpo_display;
1717
pm_check : [`Check | `WeakCheck | `Report];
18+
pm_strict_bullets : bool; (* true => bullets focus subgoals *)
1819
}
1920

2021
let dpragma = {
2122
pm_verbose = true ;
2223
pm_g_prall = false ;
2324
pm_g_prpo = EcPrinting.{ prpo_pr = false; prpo_po = false; };
2425
pm_check = `Check;
26+
pm_strict_bullets = false;
2527
}
2628

2729
module Pragma : sig
@@ -61,10 +63,15 @@ let pragma_g_po_display (b : bool) =
6163
let pragma_check mode =
6264
Pragma.upd (fun pragma -> { pragma with pm_check = mode; })
6365

66+
let pragma_strict_bullets (b : bool) =
67+
Pragma.upd (fun pragma -> { pragma with pm_strict_bullets = b; })
68+
6469
module Pragmas = struct
6570
let silent = "silent"
6671
let verbose = "verbose"
6772

73+
let strict_bullets = "strict_bullets"
74+
6875
module Proofs = struct
6976
let check = "Proofs:check"
7077
let weak = "Proofs:weak"
@@ -633,10 +640,13 @@ and process_sct_close (scope : EcScope.scope) name =
633640
(* -------------------------------------------------------------------- *)
634641
(* Add and store src for proofs *)
635642
and process_tactics ?(src : string option) (scope : EcScope.scope) t =
636-
let mode = (Pragma.get ()).pm_check in
643+
let pragma = Pragma.get () in
644+
let mode = pragma.pm_check in
645+
let strict_bullets = pragma.pm_strict_bullets in
637646
match t with
638-
| `Actual t -> snd (EcScope.Tactics.process ?src scope mode t)
639-
| `Proof -> EcScope.Tactics.proof ?src scope
647+
| `Actual (b, t) ->
648+
snd (EcScope.Tactics.process ?src ?bullet:b ~strict_bullets scope mode t)
649+
| `Proof -> EcScope.Tactics.proof ?src scope
640650

641651
(* -------------------------------------------------------------------- *)
642652
(* Add and store src for proofs *)
@@ -689,6 +699,9 @@ and process_option (scope : EcScope.scope) (name, value) =
689699
let gs = EcEnv.gstate (EcScope.env scope) in
690700
EcGState.setflag (unloc name) value gs; scope
691701

702+
| `Bool value when EcLocation.unloc name = Pragmas.strict_bullets ->
703+
pragma_strict_bullets value; scope
704+
692705
| (`Int _) as value ->
693706
let gs = EcEnv.gstate (EcScope.env scope) in
694707
EcGState.setvalue (unloc name) value gs; scope
@@ -716,14 +729,16 @@ and process_dump_why3 scope filename =
716729
EcScope.dump_why3 scope filename; scope
717730

718731
(* -------------------------------------------------------------------- *)
719-
and process_dump scope (source, tc) =
732+
and process_dump scope (source, (bullet, tc)) =
720733
let open EcCoreGoal in
721734

722735
let input, (p1, p2) = source.tcd_source in
723736

724737
let goals, scope =
725-
let mode = (Pragma.get ()).pm_check in
726-
EcScope.Tactics.process scope mode tc
738+
let pragma = Pragma.get () in
739+
let mode = pragma.pm_check in
740+
let strict_bullets = pragma.pm_strict_bullets in
741+
EcScope.Tactics.process ?bullet ~strict_bullets scope mode tc
727742
in
728743

729744
let wrerror fname =

src/ecLexer.mll

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,31 @@
301301
| '*' | '/' | '&' | '%' -> LOP3 (name |> odfl op)
302302
| _ -> LOP4 (name |> odfl op)
303303

304+
(* ------------------------------------------------------------------ *)
305+
(* Repeated bullet characters (`--`, `+++`, `***`, ...) are emitted as
306+
a single token so that the parser can distinguish them from two
307+
separate operator characters. Single-character forms keep going
308+
through the standard operator path for backward compatibility. *)
309+
let lex_bullet_chunk (op : string) =
310+
let n = String.length op in
311+
if n < 2 then None
312+
else
313+
let c = op.[0] in
314+
if not (c = '-' || c = '+' || c = '*') then None
315+
else
316+
let rec all_same i = i = n || (op.[i] = c && all_same (i+1)) in
317+
if not (all_same 1) then None
318+
else match c with
319+
| '-' -> Some (MINUSn op)
320+
| '+' -> Some (PLUSn op)
321+
| '*' -> Some (STARn op)
322+
| _ -> None
323+
304324
(* ------------------------------------------------------------------ *)
305325
let lex_operators (op : string) =
326+
match lex_bullet_chunk op with
327+
| Some tok -> [tok]
328+
| None ->
306329
let baseop (op : string) =
307330
try fst (Hashtbl.find operators op)
308331
with Not_found ->

src/ecParser.mly

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@
599599
%token WP
600600
%token ZETA
601601
%token <string> NOP LOP1 ROP1 LOP2 ROP2 LOP3 ROP3 LOP4 ROP4 NUMOP
602+
%token <string> PLUSn MINUSn STARn
602603
%token LTCOLON DASHLT GT LT GE LE LTSTARGT LTLTSTARGT LTSTARGTGT
603604
%token <Lexing.position> FINAL
604605
%token <EcParsetree.dockind * string> DOCCOMMENT
@@ -623,10 +624,10 @@
623624
%left LOP1
624625
%right ROP1
625626
%right QUESTION
626-
%left LOP2 MINUS PLUS PLUSGT
627+
%left LOP2 MINUS PLUS PLUSGT MINUSn PLUSn
627628
%right ROP2
628629
%right RARROW
629-
%left LOP3 STAR SLASH
630+
%left LOP3 STAR SLASH STARn
630631
%right ROP3
631632
%left LOP4 AT AMP HAT BACKSLASH
632633
%right ROP4
@@ -829,10 +830,12 @@ inlinepat:
829830
| LE { "<=" }
830831

831832
%inline uniop:
832-
| x=NOP { Printf.sprintf "[%s]" x }
833-
| NOT { "[!]" }
834-
| PLUS { "[+]" }
835-
| MINUS { "[-]" }
833+
| x=NOP { Printf.sprintf "[%s]" x }
834+
| NOT { "[!]" }
835+
| PLUS { "[+]" }
836+
| MINUS { "[-]" }
837+
| x=PLUSn { Printf.sprintf "[%s]" x }
838+
| x=MINUSn { Printf.sprintf "[%s]" x }
836839

837840
%inline sbinop:
838841
| EQ { "=" }
@@ -842,6 +845,9 @@ inlinepat:
842845
| STAR { "*" }
843846
| SLASH { "/" }
844847
| AT { "@" }
848+
| x=PLUSn { x }
849+
| x=MINUSn { x }
850+
| x=STARn { x }
845851
| OR { "\\/" }
846852
| ORA { "||" }
847853
| AND { "/\\" }
@@ -3577,11 +3583,17 @@ tactics0:
35773583
| ts=tactics { Pseq ts }
35783584
| x=loc(empty) { Pseq [mk_core_tactic (mk_loc x.pl_loc (Pidtac None))] }
35793585

3586+
%inline bullet:
3587+
| b=loc(MINUS) { mk_loc b.pl_loc "-" }
3588+
| b=loc(PLUS) { mk_loc b.pl_loc "+" }
3589+
| b=loc(STAR) { mk_loc b.pl_loc "*" }
3590+
| b=loc(MINUSn) { mk_loc b.pl_loc b.pl_desc }
3591+
| b=loc(PLUSn) { mk_loc b.pl_loc b.pl_desc }
3592+
| b=loc(STARn) { mk_loc b.pl_loc b.pl_desc }
3593+
35803594
toptactic:
3581-
| PLUS t=tactics { t }
3582-
| STAR t=tactics { t }
3583-
| MINUS t=tactics { t }
3584-
| t=tactics { t }
3595+
| b=bullet t=tactics { (Some b, t) }
3596+
| t=tactics { (None, t) }
35853597

35863598
tactics_or_prf:
35873599
| t=toptactic { `Actual t }

src/ecParsetree.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,8 +1364,8 @@ type global_action =
13641364
| GsctOpen of osymbol_r
13651365
| GsctClose of osymbol_r
13661366
| Grealize of prealize located
1367-
| Gtactics of [`Proof | `Actual of ptactic list]
1368-
| Gtcdump of (tcdump * ptactic list)
1367+
| Gtactics of [`Proof | `Actual of string located option * ptactic list]
1368+
| Gtcdump of (tcdump * (string located option * ptactic list))
13691369
| Gprover_info of pprover_infos
13701370
| Gsave of save located
13711371
| Gpragma of psymbol

src/ecScope.ml

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,18 @@ and proof_auc = {
296296
puc_jdg : proof_state;
297297
puc_flags : pucflags;
298298
puc_crt : EcDecl.axiom;
299+
puc_bullets : bullet_frame list;
300+
(* Active bullet stack (head = innermost). Each frame records the
301+
focused goal handle, the still-to-process sibling handles at
302+
that depth, the bullet token (e.g. "-", "--"), and the location
303+
where the bullet was opened — used for diagnostics. *)
304+
}
305+
306+
and bullet_frame = {
307+
bf_token : string;
308+
bf_loc : EcLocation.t;
309+
bf_at_open : int;
310+
bf_used : int;
299311
}
300312

301313
and proof_ctxt =
@@ -811,7 +823,108 @@ module Tactics = struct
811823
| Some src -> DocState.push_srcbl scope.sc_locdoc src
812824
| None -> scope.sc_locdoc; }
813825

814-
let process_r ?(src : string option) ?reloc mark (mode : proofmode) (scope : scope) (tac : ptactic list) =
826+
(* ----------------------------------------------------------------- *)
827+
(* Bullet-stack management. The "strict" mode (gated by the
828+
[+strict-bullets] pragma) requires each phrase to discharge its
829+
focused subgoal before moving on. The non-strict mode keeps the
830+
historical behavior where bullets are pure decoration.
831+
832+
Frames record the number of "background" open goals at the
833+
point the bullet level was opened. A frame's currently-focused
834+
subgoal is fully discharged iff [open_count <= bf_background].
835+
The bullet-level itself is fully closed when, in addition, the
836+
last sibling has been processed; we detect that by counting open
837+
goals and the depth stack. *)
838+
839+
let bullet_error ~loc fmt =
840+
let buf = Buffer.create 64 in
841+
let fbuf = Format.formatter_of_buffer buf in
842+
Format.kfprintf
843+
(fun fbuf ->
844+
Format.pp_print_flush fbuf ();
845+
hierror ~loc "%s" (Buffer.contents buf))
846+
fbuf fmt
847+
848+
let n_open juc = List.length (EcCoreGoal.all_hd_opened juc)
849+
850+
(* Validate the bullet against the current stack and return the new
851+
stack to install for this phrase. *)
852+
let open_bullet ~(bullet : string located option)
853+
(juc : EcCoreGoal.proof) (stack : bullet_frame list)
854+
: bullet_frame list =
855+
match bullet with
856+
| None -> stack
857+
| Some b ->
858+
let tok = unloc b in
859+
let loc = loc b in
860+
let opened = n_open juc in
861+
(* Search the stack from innermost outward for a frame matching
862+
[tok]. Any frames strictly inside the matched one are popped
863+
(must already be drained, i.e. opened <= bf_background). *)
864+
let rec scan acc = function
865+
| [] -> `Open
866+
| f :: rest when f.bf_token = tok -> `Match (List.rev acc, f, rest)
867+
| f :: rest -> scan (f :: acc) rest
868+
in
869+
match scan [] stack with
870+
| `Open ->
871+
(* Push a new deeper frame; this bullet consumes slot #1
872+
(out of [opened]) immediately. *)
873+
if opened = 0 then
874+
bullet_error ~loc
875+
"bullet `%s' opens a new subproof level but there are \
876+
no remaining subgoals" tok;
877+
let frame = {
878+
bf_token = tok;
879+
bf_loc = loc;
880+
bf_at_open = opened;
881+
bf_used = 1;
882+
} in
883+
frame :: stack
884+
| `Match (inner, frame, outer) ->
885+
(* The expected open count after fully discharging the
886+
previous slot, given the frame's history. *)
887+
let expected = frame.bf_at_open - frame.bf_used in
888+
(* Each inner frame must already be drained — i.e., its
889+
outer view of the open count must be reachable. *)
890+
List.iter (fun (f : bullet_frame) ->
891+
if opened > f.bf_at_open - f.bf_used then
892+
bullet_error ~loc
893+
"bullet `%s' (matches an outer level opened at %s) \
894+
skips past inner bullet `%s' opened at %s whose \
895+
subproof is not closed"
896+
tok (EcLocation.tostring frame.bf_loc)
897+
f.bf_token (EcLocation.tostring f.bf_loc))
898+
inner;
899+
(* The matching frame's current slot must be closed. *)
900+
if opened > expected then
901+
bullet_error ~loc
902+
"bullet `%s' reused but the previous subgoal (opened at \
903+
%s) is not closed"
904+
tok (EcLocation.tostring frame.bf_loc);
905+
(* Slots remaining? *)
906+
if frame.bf_used >= frame.bf_at_open then
907+
bullet_error ~loc
908+
"bullet `%s' reused but no sibling subgoals remain at \
909+
this level (opened at %s)"
910+
tok (EcLocation.tostring frame.bf_loc);
911+
let frame = { frame with
912+
bf_loc = loc;
913+
bf_used = frame.bf_used + 1;
914+
} in
915+
frame :: outer
916+
917+
(* Close the phrase: validate that the (possibly bulleted) phrase
918+
made progress on the focused subgoal. We do NOT pop drained
919+
frames here; that's done at the next [open_bullet] or on save. *)
920+
let close_bullet ~(bullet : _) (juc : EcCoreGoal.proof)
921+
(stack : bullet_frame list) : bullet_frame list =
922+
let _ = bullet in let _ = juc in
923+
stack
924+
925+
let process_r ?(src : string option) ?(bullet : string located option)
926+
?(strict_bullets : bool = false)
927+
?reloc mark (mode : proofmode) (scope : scope) (tac : ptactic list) =
815928
check_state `InProof "proof script" scope;
816929

817930
let scope =
@@ -855,6 +968,11 @@ module Tactics = struct
855968
EcHiGoal.tt_redlogic = Options.get_redlogic scope;
856969
EcHiGoal.tt_und_delta = Options.get_und_delta scope; } in
857970

971+
let bullets =
972+
if strict_bullets then open_bullet ~bullet juc pac.puc_bullets
973+
else pac.puc_bullets
974+
in
975+
858976
let (hds, juc) =
859977
try TTC.process ttenv tac juc
860978
with EcCoreGoal.TcError tcerror ->
@@ -866,9 +984,14 @@ module Tactics = struct
866984
in raise (EcCoreGoal.TcError tcerror)
867985
in
868986

987+
let bullets =
988+
if strict_bullets then close_bullet ~bullet juc bullets
989+
else bullets
990+
in
991+
869992
let penv = EcCoreGoal.proofenv_of_proof juc in
870993

871-
let pac = { pac with puc_jdg = PSCheck juc } in
994+
let pac = { pac with puc_jdg = PSCheck juc; puc_bullets = bullets } in
872995
let puc = { puc with puc_active = Some (pac, pct); } in
873996
let scope = { scope with sc_pr_uc = Some puc; } in
874997
Some (penv, hds), scope
@@ -880,8 +1003,9 @@ module Tactics = struct
8801003
let ts = List.map (fun t -> { pt_core = t; pt_intros = []; }) ts in
8811004
snd (process_r mark mode scope ts)
8821005

883-
let process ?(src : string option) scope mode tac =
884-
process_r ?src true mode scope tac
1006+
let process ?(src : string option) ?(bullet : string located option)
1007+
?(strict_bullets : bool = false) scope mode tac =
1008+
process_r ?src ?bullet ~strict_bullets true mode scope tac
8851009
end
8861010

8871011
(* -------------------------------------------------------------------- *)
@@ -955,7 +1079,8 @@ module Ax = struct
9551079
; puc_started = false
9561080
; puc_jdg = puc
9571081
; puc_flags = axflags
958-
; puc_crt = axd }
1082+
; puc_crt = axd
1083+
; puc_bullets = [] }
9591084
in
9601085
{ puc_active = Some (active, ctxt);
9611086
puc_cont = cont;

0 commit comments

Comments
 (0)