Skip to content

Commit 76bf9e9

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 (--) ...`). The final sibling of a split point can be continued at the parent level without a bullet: when the previous sibling is closed and exactly one sibling remains, the inner frame pops automatically (and cascades through nested last-sibling frames). This keeps the standard phl idiom of long `seq N : <post>' chains flat instead of forcing ever-deeper indentation under a `+' for each continuation. Multi-way splits keep their enumeration discipline for all but the last subgoal.
1 parent c65e627 commit 76bf9e9

16 files changed

Lines changed: 612 additions & 38 deletions

src/ecBullets.ml

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
(* -------------------------------------------------------------------- *)
2+
open EcLocation
3+
open EcParsetree
4+
5+
(* -------------------------------------------------------------------- *)
6+
type frame = {
7+
bf_bullet : bullet;
8+
bf_loc : EcLocation.t;
9+
bf_floor : int;
10+
}
11+
12+
type stack = frame list
13+
14+
type error =
15+
| UnbulletedSplit of { opened : int; level : [`Top | `Frame of frame] }
16+
| NoSubgoalToOpen of { bullet : bullet }
17+
| OuterSkipsInner of { bullet : bullet; outer : frame; inner : frame }
18+
| ReuseBeforeClosing of { bullet : bullet; frame : frame }
19+
20+
exception BulletError of EcLocation.t option * error
21+
22+
(* -------------------------------------------------------------------- *)
23+
let pp_bullet fmt (b : bullet) =
24+
let c = match b.b_kind with `Minus -> '-' | `Plus -> '+' | `Star -> '*' in
25+
for _ = 1 to b.b_count do Format.pp_print_char fmt c done
26+
27+
let pp_error fmt = function
28+
| UnbulletedSplit { opened; level = `Top } ->
29+
Format.fprintf fmt
30+
"previous tactic left %d open subgoals at top level; the next \
31+
phrase needs a bullet to focus one of them" opened
32+
| UnbulletedSplit { opened; level = `Frame f } ->
33+
Format.fprintf fmt
34+
"previous tactic left %d open subgoals at the bullet level \
35+
opened by `%a' at %s; the next phrase needs a bullet to \
36+
focus one of them"
37+
opened pp_bullet f.bf_bullet (EcLocation.tostring f.bf_loc)
38+
| NoSubgoalToOpen { bullet } ->
39+
Format.fprintf fmt
40+
"bullet `%a' opens a new subproof level but there are no \
41+
remaining subgoals" pp_bullet bullet
42+
| OuterSkipsInner { bullet; outer; inner } ->
43+
Format.fprintf fmt
44+
"bullet `%a' (matches an outer level opened at %s) skips past \
45+
inner bullet `%a' opened at %s whose subproof is not closed"
46+
pp_bullet bullet (EcLocation.tostring outer.bf_loc)
47+
pp_bullet inner.bf_bullet (EcLocation.tostring inner.bf_loc)
48+
| ReuseBeforeClosing { bullet; frame } ->
49+
Format.fprintf fmt
50+
"bullet `%a' reused but the previous subgoal (opened at %s) \
51+
is not closed"
52+
pp_bullet bullet (EcLocation.tostring frame.bf_loc)
53+
54+
(* -------------------------------------------------------------------- *)
55+
let n_open (juc : EcCoreGoal.proof) =
56+
List.length (EcCoreGoal.all_hd_opened juc)
57+
58+
let raise_error ?loc err = raise (BulletError (loc, err))
59+
60+
(* -------------------------------------------------------------------- *)
61+
(* Validate the bullet against the current stack and return the
62+
pre-phrase stack. Each frame's [bf_floor] records the open-count
63+
that should remain once the frame's subproof is fully closed; the
64+
frame becomes poppable when [n_open <= bf_floor]. *)
65+
let open_phrase ~(bullet : bullet located option)
66+
(juc : EcCoreGoal.proof) (stack : stack) : stack =
67+
let opened = n_open juc in
68+
(* Top-of-stack floor, or 0 if the stack is empty (top-level: one
69+
focused goal allowed without a bullet). A phrase may run
70+
unbulleted iff [opened <= floor_top + 1]: the focused goal
71+
plus the goals "outside" the current level. *)
72+
let floor_top =
73+
match stack with [] -> 0 | f :: _ -> f.bf_floor in
74+
match bullet with
75+
| None ->
76+
if opened > floor_top + 1 then begin
77+
let level =
78+
match stack with [] -> `Top | f :: _ -> `Frame f in
79+
raise_error (UnbulletedSplit { opened; level })
80+
end;
81+
stack
82+
| Some b ->
83+
let bul = unloc b in
84+
let loc = loc b in
85+
(* Search the stack from innermost outward for a frame matching
86+
[bul]. Inner frames not yet drained block the match. *)
87+
let rec scan acc = function
88+
| [] -> `Open
89+
| f :: rest when f.bf_bullet = bul -> `Match (List.rev acc, f, rest)
90+
| f :: rest -> scan (f :: acc) rest
91+
in
92+
match scan [] stack with
93+
| `Open ->
94+
if opened = 0 then
95+
raise_error ~loc (NoSubgoalToOpen { bullet = bul });
96+
{ bf_bullet = bul; bf_loc = loc; bf_floor = opened - 1 } :: stack
97+
| `Match (inner, frame, outer) ->
98+
(* Inner frames must already be drained. *)
99+
List.iter (fun (f : frame) ->
100+
if opened > f.bf_floor then
101+
raise_error ~loc
102+
(OuterSkipsInner { bullet = bul; outer = frame; inner = f }))
103+
inner;
104+
(* The matching frame's current slot must be closed. *)
105+
if opened > frame.bf_floor then
106+
raise_error ~loc (ReuseBeforeClosing { bullet = bul; frame });
107+
{ frame with bf_loc = loc } :: outer
108+
109+
(* -------------------------------------------------------------------- *)
110+
(* After a phrase has run, pop frames whose subproof has fully
111+
closed. Cascades through nested last-sibling frames; this is what
112+
lets the last sibling at any level be addressed by an unbulleted
113+
phrase. *)
114+
let close_phrase (juc : EcCoreGoal.proof) (stack : stack) : stack =
115+
let opened = n_open juc in
116+
let rec pop = function
117+
| f :: rest when opened <= f.bf_floor -> pop rest
118+
| s -> s
119+
in
120+
pop stack
121+
122+
(* -------------------------------------------------------------------- *)
123+
let pp_exn fmt = function
124+
| BulletError (_, err) -> pp_error fmt err
125+
| exn -> raise exn
126+
127+
let () = EcPException.register pp_exn

src/ecBullets.mli

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
(* -------------------------------------------------------------------- *)
2+
(* Bullet-stack management for [+strict_bullets]. A frame is pushed
3+
on the stack each time the user opens a new bullet level, and
4+
popped after a phrase whose subproof has fully closed (cascading
5+
through nested last-sibling frames). *)
6+
7+
open EcLocation
8+
open EcParsetree
9+
10+
(* A single bullet frame: the bullet that opened it, the location of
11+
that opening, and the open-goal count that should remain once
12+
this frame's subproof is fully discharged. *)
13+
type frame = private {
14+
bf_bullet : bullet;
15+
bf_loc : EcLocation.t;
16+
bf_floor : int;
17+
}
18+
19+
type stack = frame list
20+
21+
(* Structured description of every way a strict-bullet check can
22+
fail. Each constructor carries enough context for diagnostics
23+
without forcing the caller to pattern-match on rendered strings. *)
24+
type error =
25+
| UnbulletedSplit of { opened : int; level : [`Top | `Frame of frame] }
26+
| NoSubgoalToOpen of { bullet : bullet }
27+
| OuterSkipsInner of { bullet : bullet; outer : frame; inner : frame }
28+
| ReuseBeforeClosing of { bullet : bullet; frame : frame }
29+
30+
exception BulletError of EcLocation.t option * error
31+
32+
(* Render a bullet as its surface syntax, e.g. ["-"], ["+++"]. *)
33+
val pp_bullet : Format.formatter -> bullet -> unit
34+
35+
val pp_error : Format.formatter -> error -> unit
36+
37+
(* Validate the optional bullet on the next phrase against [stack],
38+
returning the stack to install for that phrase. May raise
39+
[BulletError]. *)
40+
val open_phrase :
41+
bullet:bullet located option
42+
-> EcCoreGoal.proof
43+
-> stack
44+
-> stack
45+
46+
(* Pop frames whose subproof has fully closed after the last phrase.
47+
Cascades through nested last-sibling frames. *)
48+
val close_phrase : EcCoreGoal.proof -> stack -> stack

src/ecCommands.ml

Lines changed: 21 additions & 7 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"
@@ -505,7 +512,9 @@ and process_abbrev (scope : EcScope.scope) (a : pabbrev located) =
505512
(* -------------------------------------------------------------------- *)
506513
and process_axiom ?(src : string option) (scope : EcScope.scope) (ax : paxiom located) =
507514
EcScope.check_state `InTop "axiom" scope;
508-
let (name, scope) = EcScope.Ax.add ?src scope (Pragma.get ()).pm_check ax in
515+
let pragma = Pragma.get () in
516+
let strict = pragma.pm_strict_bullets in
517+
let (name, scope) = EcScope.Ax.add ?src ~strict scope pragma.pm_check ax in
509518
name |> EcUtils.oiter
510519
(fun x ->
511520
match (unloc ax).pa_kind with
@@ -635,8 +644,9 @@ and process_sct_close (scope : EcScope.scope) name =
635644
and process_tactics ?(src : string option) (scope : EcScope.scope) t =
636645
let mode = (Pragma.get ()).pm_check 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 scope mode t)
649+
| `Proof -> EcScope.Tactics.proof ?src scope
640650

641651
(* -------------------------------------------------------------------- *)
642652
(* Add and store src for proofs *)
@@ -653,8 +663,9 @@ and process_save ?(src : string option) (scope : EcScope.scope) ed =
653663

654664
(* -------------------------------------------------------------------- *)
655665
and process_realize (scope : EcScope.scope) pr =
656-
let mode = (Pragma.get ()).pm_check in
657-
let (name, scope) = EcScope.Ax.realize scope mode pr in
666+
let pragma = Pragma.get () in
667+
let strict = pragma.pm_strict_bullets in
668+
let (name, scope) = EcScope.Ax.realize ~strict scope pragma.pm_check pr in
658669
name |> EcUtils.oiter
659670
(fun x -> EcScope.notify scope `Info "added lemma: `%s'" x);
660671
scope
@@ -689,6 +700,9 @@ and process_option (scope : EcScope.scope) (name, value) =
689700
let gs = EcEnv.gstate (EcScope.env scope) in
690701
EcGState.setflag (unloc name) value gs; scope
691702

703+
| `Bool value when EcLocation.unloc name = Pragmas.strict_bullets ->
704+
pragma_strict_bullets value; scope
705+
692706
| (`Int _) as value ->
693707
let gs = EcEnv.gstate (EcScope.env scope) in
694708
EcGState.setvalue (unloc name) value gs; scope
@@ -716,14 +730,14 @@ and process_dump_why3 scope filename =
716730
EcScope.dump_why3 scope filename; scope
717731

718732
(* -------------------------------------------------------------------- *)
719-
and process_dump scope (source, tc) =
733+
and process_dump scope (source, (bullet, tc)) =
720734
let open EcCoreGoal in
721735

722736
let input, (p1, p2) = source.tcd_source in
723737

724738
let goals, scope =
725739
let mode = (Pragma.get ()).pm_check in
726-
EcScope.Tactics.process scope mode tc
740+
EcScope.Tactics.process ?bullet scope mode tc
727741
in
728742

729743
let wrerror fname =

src/ecLexer.mll

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,23 @@
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 EcRegexp.match_ (`S "^-{2,}$" ) op then Some (MINUSn n)
312+
else if EcRegexp.match_ (`S "^\\+{2,}$") op then Some (PLUSn n)
313+
else if EcRegexp.match_ (`S "^\\*{2,}$") op then Some (STARn n)
314+
else None
315+
304316
(* ------------------------------------------------------------------ *)
305317
let lex_operators (op : string) =
318+
match lex_bullet_chunk op with
319+
| Some tok -> [tok]
320+
| None ->
306321
let baseop (op : string) =
307322
try fst (Hashtbl.find operators op)
308323
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 <int> 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+
| n=PLUSn { Printf.sprintf "[%s]" (String.make n '+') }
838+
| n=MINUSn { Printf.sprintf "[%s]" (String.make n '-') }
836839

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

3598+
%inline bullet:
3599+
| b=loc(MINUS) { mk_loc b.pl_loc { b_kind = `Minus; b_count = 1 } }
3600+
| b=loc(PLUS) { mk_loc b.pl_loc { b_kind = `Plus ; b_count = 1 } }
3601+
| b=loc(STAR) { mk_loc b.pl_loc { b_kind = `Star ; b_count = 1 } }
3602+
| b=loc(MINUSn) { mk_loc b.pl_loc { b_kind = `Minus; b_count = b.pl_desc } }
3603+
| b=loc(PLUSn) { mk_loc b.pl_loc { b_kind = `Plus ; b_count = b.pl_desc } }
3604+
| b=loc(STARn) { mk_loc b.pl_loc { b_kind = `Star ; b_count = b.pl_desc } }
3605+
35923606
toptactic:
3593-
| PLUS t=tactics { t }
3594-
| STAR t=tactics { t }
3595-
| MINUS t=tactics { t }
3596-
| t=tactics { t }
3607+
| b=bullet t=tactics { (Some b, t) }
3608+
| t=tactics { (None, t) }
35973609

35983610
tactics_or_prf:
35993611
| t=toptactic { `Actual t }

src/ecParsetree.ml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ type pgamepath = (pmsymbol * psymbol) located
3636
type osymbol_r = psymbol option
3737
type osymbol = osymbol_r located
3838

39+
(* -------------------------------------------------------------------- *)
40+
(* A bullet at the start of a `.`-terminated tactic phrase. The kind
41+
identifies the bullet character (`-`, `+`, `*`); the count is the
42+
number of repetitions (`>= 1`). *)
43+
type bullet_kind = [ `Minus | `Plus | `Star ]
44+
type bullet = { b_kind : bullet_kind; b_count : int; }
45+
3946
(* -------------------------------------------------------------------- *)
4047
type pcp_match = [
4148
| `If
@@ -1370,8 +1377,8 @@ type global_action =
13701377
| GsctOpen of osymbol_r
13711378
| GsctClose of osymbol_r
13721379
| Grealize of prealize located
1373-
| Gtactics of [`Proof | `Actual of ptactic list]
1374-
| Gtcdump of (tcdump * ptactic list)
1380+
| Gtactics of [`Proof | `Actual of bullet located option * ptactic list]
1381+
| Gtcdump of (tcdump * (bullet located option * ptactic list))
13751382
| Gprover_info of pprover_infos
13761383
| Gsave of save located
13771384
| Gpragma of psymbol

0 commit comments

Comments
 (0)