forked from FStarLang/fstar-mode.el
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfstar-mode.el
executable file
·5558 lines (4831 loc) · 223 KB
/
fstar-mode.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; fstar-mode.el --- Support for F* programming -*- lexical-binding: t; indent-tabs-mode: nil -*-
;; Copyright (C) 2015-2017 Clément Pit-Claudel
;; Author: Clément Pit-Claudel <clement.pitclaudel@live.com>
;; URL: https://github.com/FStarLang/fstar-mode.el
;; Created: 27 Aug 2015
;; Version: 0.4
;; Package-Requires: ((emacs "24.3") (dash "2.11") (company "0.8.12") (quick-peek "1.0") (yasnippet "0.11.0") (flycheck "30.0") (company-quickhelp "2.2.0"))
;; Keywords: convenience, languages
;; This file is not part of GNU Emacs.
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
;;; Commentary:
;; This file implements support for F* programming in Emacs, including:
;;
;; * Syntax highlighting
;; * Unicode math (prettify-symbols-mode)
;; * Documentation and search
;; * Relative indentation
;; * Outlining and selective display
;; * Type hints (Eldoc)
;; * Autocompletion (Company)
;; * Type-aware snippets (Yasnippet)
;; * Interactive proofs (à la Proof-General)
;; * Real-time verification (Flycheck)
;; * Remote editing (Tramp)
;;
;; See https://github.com/FStarLang/fstar-mode.el for setup and usage tips.
;;; Code:
;;; Imports
(require 'cl-lib)
(require 'json)
(require 'eldoc)
(require 'help-at-pt)
(require 'ansi-color)
(require 'easymenu)
(require 'tramp)
(require 'tramp-sh)
(require 'crm)
(require 'tool-bar)
(require 'notifications nil t)
;; replace.el doesn't `provide' in Emacs < 26
(ignore-errors (require 'replace))
(require 'dash)
(require 'company)
(require 'quick-peek)
(require 'yasnippet)
(require 'flycheck)
(require 'let-alist)
(require 'company-quickhelp)
(defconst fstar--script-full-path
(or (and load-in-progress load-file-name)
(bound-and-true-p byte-compile-current-file)
(buffer-file-name))
"Full path of this script.")
(defconst fstar--directory
(file-name-directory fstar--script-full-path)
"Full path to directory of this script.")
;;; Compatibility with older Emacsen
(defmacro fstar-assert (&rest args)
"Call `cl-assert' on ARGS, if available."
(declare (debug t))
`(when (fboundp 'cl-assert)
(cl-assert ,@args)))
(eval-and-compile
(unless (featurep 'subr-x)
(defsubst string-trim-left (string)
"Remove leading whitespace from STRING."
(if (string-match "\\`[ \t\n\r]+" string)
(replace-match "" t t string)
string))
(defsubst string-trim-right (string)
"Remove trailing whitespace from STRING."
(if (string-match "[ \t\n\r]+\\'" string)
(replace-match "" t t string)
string)))
(unless (fboundp 'prog-widen)
(defalias 'prog-widen 'widen)))
(defun fstar--string-trim (s)
"Trim S, or return nil if nil."
(when s (string-trim-left (string-trim-right s))))
;;; Customization
(defgroup fstar nil
"F* mode."
:group 'languages)
(defgroup fstar-literate nil
"Literate programming support for F* mode."
:group 'fstar)
(defgroup fstar-interactive nil
"Interactive proofs."
:group 'fstar)
(defgroup fstar-tactics nil
"Tactics and tactic-based proofs."
:group 'fstar-interactive)
(defvaralias 'flycheck-fstar-executable 'fstar-executable)
(make-obsolete-variable 'flycheck-fstar-executable 'fstar-executable "0.2" 'set)
(defcustom fstar-executable "fstar.exe"
"Where to find F*.
May be either “fstar.exe” (search in $PATH) or an absolute path."
:group 'fstar
:type 'file
:risky t)
(defvaralias 'flycheck-fstar-literate-executable 'fstar-python-executable)
(defcustom fstar-python-executable "python"
"Where to find Python (preferably 3).
Can be \"python\" or \"python3\", or an absolute path."
:group 'fstar
:type 'file
:risky t)
(defconst fstar-known-modules
'((font-lock . "Syntax highlighting")
(prettify . "Unicode math (e.g. display forall as ∀; requires emacs 24.4 or later)")
(subscripts . #("Pretty susbcripts (e.g. display x1 as x1)" 39 40 (display (raise -0.3))))
(indentation . "Indentation (relative to previous lines)")
(comments . "Comment syntax and special comments ('(***', '(*+', etc.)")
(flycheck . "Real-time verification with Flycheck.")
(interactive . "Interactive verification (à la Proof-General).")
(eldoc . "Type annotations in the minibuffer.")
(company . "Completion with company-mode.")
(company-defaults . "Opinionated company-mode configuration.")
(spinner . "Blink the modeline while F* is busy.")
(notifications . "Show a notification when a proof completes.")
(literate . "Prettify “///” comment markers.")
(literate-flycheck . "Real-time syntax-checking for literate comments.")
(overlay-legend . "Show a legend in the modeline when hovering an F* overlay.")
(tool-bar . "Customize the toolbar for interactive use of F*.")
(auto-insert . "Support for `auto-insert-mode'."))
"Available components of F*-mode.")
(defcustom fstar-enabled-modules
'(font-lock prettify subscripts indentation comments flycheck interactive
eldoc company company-defaults spinner notifications
literate literate-flycheck overlay-legend tool-bar auto-insert)
"Which F*-mode components to load."
:group 'fstar
:type `(set ,@(cl-loop for (mod . desc) in fstar-known-modules
collect `(const :tag ,desc ,mod))))
;;; Type definitions
;; `fstar-location' must be defined early to reference it in `cl-typecase'
(cl-defstruct fstar-location
filename line-from line-to col-from col-to)
(defun fstar--expand-file-name-on-remote (path)
"Expand PATH and add Tramp's prefix to it if needed."
(setq path (expand-file-name path))
(if (file-remote-p path) path
(concat (or (fstar--remote-p) "") path)))
(defun fstar-location-remote-filename (loc)
"Expand LOC's filename and add Tramp's prefix to it if needed."
(fstar--expand-file-name-on-remote (fstar-location-filename loc)))
(defun fstar--loc-to-string (loc)
"Turn LOC into a string."
(format "%s(%d,%d-%d,%d)"
(fstar-location-remote-filename loc)
(fstar-location-line-from loc)
(fstar-location-col-from loc)
(fstar-location-line-to loc)
(fstar-location-col-to loc)))
(defun fstar-location-beg-offset (location)
"Compute LOCATION's beginning offset in the current buffer."
(fstar--line-col-offset (fstar-location-line-from location)
(fstar-location-col-from location)))
(defun fstar-location-end-offset (location)
"Compute LOCATION's end offset in the current buffer."
(fstar--line-col-offset (fstar-location-line-to location)
(fstar-location-col-to location)))
(cl-defstruct fstar-continuation
id body start-time)
(defun fstar-continuation--delay (continuation)
"Return the time elapsed since CONTINUATION was created."
(time-since (fstar-continuation-start-time continuation)))
;;; Utilities
(defconst fstar--spaces "\t\n\r ")
(defun fstar--indent-str (str amount &optional skip-first-line)
"Indent all lines of STR by AMOUNT.
If AMOUNT is a string, use that as indentation.
With SKIP-FIRST-LINE, don't indent the first one."
(let ((indent (if (stringp amount) amount (make-string amount ?\s))))
(if skip-first-line
(replace-regexp-in-string "\n" (concat "\n" indent) str)
(replace-regexp-in-string "^" indent str))))
(defun fstar--property-range (pos prop)
"Find range around POS with consistent non-nil value of PROP."
(let* ((val-1 (get-text-property (1- pos) prop))
(val (get-text-property pos prop)))
(when (or val val-1)
(list (if (eq (or val val-1) val-1)
(previous-single-property-change pos prop) pos)
(if val
(next-single-property-change pos prop) pos)
(or val val-1)))))
(defun fstar--expand-snippet (&rest args)
"Ensure that Yasnippet is on and forward ARGS to `yas-expand-snippet'."
(unless yas-minor-mode (yas-minor-mode 1))
(let ((yas-indent-line nil))
(apply #'yas-expand-snippet args)))
(defun fstar--refresh-eldoc ()
"Tell Eldoc to do its thing.
It's often incorrect to add commands to `eldoc-message-commands',
as that leads to outdated information being displayed when the
command is asynchronous. For example, adding
`fstar-insert-match-dwim' to `eldoc-message-commands' causes
eldoc to show the type of the hole *before* destruction, not
after."
(eldoc-print-current-symbol-info))
(defun fstar--unwrap-paragraphs (str)
"Remove hard line wraps from STR."
(replace-regexp-in-string " *\n\\([^\n\t ]\\)" " \\1" str))
(defun fstar--strip-newlines (str)
"Remove all newlines from STR."
(replace-regexp-in-string " *\n *" " " str))
(defun fstar--resolve-fn-value (fn-or-v)
"Return FN-OR-V, or the result of calling it if it's a function."
(if (functionp fn-or-v)
(funcall fn-or-v)
fn-or-v))
(defun fstar--remote-p ()
"Check if current buffer is remote.
Return a file name prefix if so, and nil otherwise."
(file-remote-p (or buffer-file-name default-directory)))
(defun fstar--local-p ()
"Check if current buffer is local."
(not (fstar--remote-p)))
(defun fstar--maybe-cygpath (path)
"Translate PATH using Cygpath if appropriate."
(if (and (eq system-type 'cygwin) (fstar--local-p))
(string-trim-right (car (process-lines "cygpath" "-w" path)))
path))
(defun fstar--hide-buffer (buf)
"Hide window displaying BUF, if any.
Return value indicates whether a window was hidden."
(-when-let* ((doc-wins (and (buffer-live-p (get-buffer buf))
(get-buffer-window-list buf nil t))))
(mapc (apply-partially #'quit-window nil) doc-wins)
t))
(defun fstar--read-string (prompt default)
"Read a string with PROMPT and DEFAULT.
Prompt should have one string placeholder to accommodate DEFAULT."
(let ((default-info (if default (format " (default ‘%s’)" default) "")))
(setq prompt (format prompt default-info)))
(read-string prompt nil nil default))
(defun fstar--syntax-ppss (&optional pos)
"Like `syntax-ppss' at POS, but don't move point."
;; This can be called in a narrowed buffer by `blink-matching-open'.
(save-match-data (save-excursion (syntax-ppss pos))))
(defun fstar-in-comment-p (&optional pos)
"Return non-nil if POS is inside a comment."
(nth 4 (fstar--syntax-ppss pos)))
(defconst fstar--literate-comment-re "^///\\( \\|$\\)"
"Regexp matching literate comment openers.")
(defun fstar-in-literate-comment-p (&optional pos)
"Return non-nil if POS is inside a literate comment."
(save-excursion
(goto-char (or pos (point)))
(goto-char (point-at-bol))
(looking-at-p fstar--literate-comment-re)))
(defun fstar--column-in-commment-p (column)
"Return non-nil if point at COLUMN is inside a comment."
(save-excursion
(move-to-column column)
(fstar-in-comment-p)))
(defvar fstar--syntax-table-for--delimited-by
(let ((table (make-syntax-table)))
(modify-syntax-entry ?\" "." table)
table))
(defun fstar--delimited-by-p (delim pos limit)
"Check if POS is enclosed in DELIM before LIMIT."
(let ((found nil))
(save-restriction
(narrow-to-region limit (point-max))
(with-syntax-table fstar--syntax-table-for--delimited-by
(while (and (not found)
(setq pos (ignore-errors (scan-lists pos -1 1))))
(setq found (eq (char-after pos) delim)))))
found))
(defun fstar--in-string-p (&optional pos)
"Check if POS is in a string.
This doesn't work for strings in snippets inside of comments."
(nth 3 (fstar--syntax-ppss pos)))
(defun fstar--in-code-p (&optional pos)
"Check if POS is in a code fragment (possibly embedded in a comment)."
(let* ((sx (fstar--syntax-ppss pos))
(in-comment (nth 4 sx)))
(or (not in-comment)
(and
(not (fstar-in-literate-comment-p))
(save-excursion
(when pos (goto-char pos))
(let ((comment-beg (nth 8 sx)))
(fstar--delimited-by-p
?\[ (point) (max comment-beg (point-at-bol)))))))))
(defun fstar-subp--column-number-at-pos (pos)
"Return column number at POS."
(save-excursion (goto-char pos) (- (point) (point-at-bol))))
(defun fstar--goto-line-col (line &optional column)
"Go to position indicated by LINE, COLUMN."
(goto-char (point-min))
(forward-line (1- line))
(when column
;; min makes sure that we don't spill to the next line.
(forward-char (min (- (point-at-eol) (point-at-bol)) column))))
(defun fstar--line-col-offset (line column)
"Convert a (LINE, COLUMN) pair into a buffer offset."
;; LATER: This would be much easier if the interactive mode returned
;; an offset instead of a line an column.
(save-excursion
(fstar--goto-line-col line column)
(point)))
(defun fstar--match-strings-no-properties (ids &optional str)
"Get (match-string-no-properties ID STR) for each ID in IDS."
(mapcar (lambda (num) (match-string-no-properties num str)) ids))
(defvar fstar--fqn-at-point-syntax-table)
(defun fstar--fqn-at-point (&optional pos)
"Return symbol at POS (default: point)."
(setq pos (or pos (point)))
(with-syntax-table fstar--fqn-at-point-syntax-table
(save-excursion
(goto-char pos)
(-when-let* ((s (thing-at-point 'symbol t)))
(replace-regexp-in-string ;; Drop final "." from e.g. A.B.(xy)
"\\.\\'" "" s)))))
(defun fstar--propertize-title (title)
"Format TITLE as a title."
(propertize title 'face '(:height 1.5)))
(defun fstar--push-mark ()
"Save current position in mark ring and xref stack."
(push-mark nil t)
(when (fboundp 'xref-push-marker-stack)
(xref-push-marker-stack)))
(defun fstar--navigate-to-parse-display-action (display-action)
"Convert DISPLAY-ACTION into an argument to `display-buffer'."
(pcase display-action
(`nil '((display-buffer-same-window)))
(`reuse '((display-buffer-reuse-window display-buffer-same-window)))
(`window '((display-buffer-reuse-window display-buffer-pop-up-window)
(reusable-frames . 0)
(inhibit-same-window . t)))
(`frame '((display-buffer-reuse-window display-buffer-pop-up-frame)
(reusable-frames . 0)
(inhibit-same-window . t)))))
(defun fstar--highlight-region (beg end)
"Pulse BEG..END.
If END is nil, pulse the entire line containing BEG."
(if end
(when (fboundp 'pulse-momentary-highlight-region)
(pulse-momentary-highlight-region beg end))
(when (fboundp 'pulse-momentary-highlight-one-line)
(pulse-momentary-highlight-one-line beg))))
(defvar-local fstar--parent-buffer nil
"The buffer that opened the current buffer, if it exists.
This is relevant when (for example) a user jumps to the current
buffer using a 'jump to definition' command. If it is set, then
this buffer may be used to run F* queries if an F* process isn't
started in the current buffer.")
(defun fstar--navigate-to-1 (location display-action switch)
"Navigate to LOCATION.
DISPLAY-ACTION determines where the resulting buffer is
shown (nil for the currently selected window, `reuse' for the
current window unless another one is already showing the buffer,
`window' for a separate window, and `frame' for a separate
frame). SWITCH determines whether the resulting buffer and
window become current and selected."
(fstar--push-mark)
(let ((fname (fstar-location-remote-filename location))
(line (fstar-location-line-from location))
(col (fstar-location-col-from location))
(line-to (fstar-location-line-to location))
(col-to (fstar-location-col-to location))
(action (fstar--navigate-to-parse-display-action display-action)))
(if (not (file-exists-p fname))
(message "File not found: %S" fname)
(-when-let* ((buf (find-file-noselect fname))
(parent-buf (or fstar--parent-buffer (current-buffer)))
(win (if (not switch)
(display-buffer buf action)
(when (eq (pop-to-buffer buf action)
(window-buffer (selected-window)))
(selected-window)))))
(with-selected-window win
(with-current-buffer buf ;; FIXME check this
(push-mark (point) t) ;; Save default position in mark ring
(setq fstar--parent-buffer parent-buf)
(fstar--goto-line-col (or line 1) col)
(recenter)
(when line
(let ((end (and line-to (fstar--line-col-offset line-to col-to))))
(fstar--highlight-region (point) end)))))))))
(defun fstar--navigate-to (target &optional display-action)
"Jump to TARGET, a location or a path.
DISPLAY-ACTION: see `fstar--navigate-to-1'."
(fstar--navigate-to-1
(cl-etypecase target
(fstar-location target)
(string (make-fstar-location
:filename target
:line-from nil :line-to nil
:col-from nil :col-to nil)))
display-action t))
(defun fstar--visit-link-target (marker)
"Jump to file indicated by entry at MARKER."
(let* ((pos (marker-position marker))
(buf (marker-buffer marker))
(target (get-text-property pos 'fstar--target buf))
(display-action (get-text-property pos 'fstar--display-action buf)))
(fstar--navigate-to target display-action)))
(defun fstar--insert-link (target face &optional label display-action)
"Insert a link to TARGET with LABEL and FACE.
TARGET is either a string or a location.
DISPLAY-ACTION: see `fstar--navigate-to-1'."
(let ((label (or label
(cl-typecase target
(string target)
(fstar-location (fstar--loc-to-string target))))))
(insert-text-button label 'fstar--target target
'face face 'follow-link t
'action 'fstar--visit-link-target
'fstar--display-action display-action)))
(defun fstar--lispify-null (x)
"Return X, or nil if X is `:json-null'."
(unless (eq x :json-null) x))
(defun fstar--lispify-false (x)
"Return X, or nil if X is `:json-false'."
(unless (eq x :json-false) x))
(defun fstar--comment-beginning (pos)
"Return point before start of comment at POS."
(nth 8 (fstar--syntax-ppss pos)))
(defun fstar--search-predicated-1 (search-fn test-fn move-fn re bound)
"Helper for `fstar--search-predicated'.
SEARCH-FN, TEST-FN, MOVE-FN, RE, BOUND: see `fstar--search-predicated'."
(catch 'found
(while (funcall search-fn re bound t)
(when (funcall test-fn)
(throw 'found (point)))
(when (= (match-beginning 0) (match-end 0))
(unless (funcall move-fn)
(throw 'found nil))))
(throw 'found nil)))
(defun fstar--search-predicated (search-fn test-fn move-fn re bound)
"Use SEARCH-FN to find matches of RE before BOUND satisfying TEST-FN.
MOVE-FN is used to move the point when TEST-FN returns nil and
the match is empty. Return non-nil if RE can be found. This
function does not move the point."
(save-excursion
(comment-normalize-vars)
(and (fstar--search-predicated-1 search-fn test-fn move-fn re bound)
;; Ensure that the match is maximal when searching backwards for
;; e.g. \n+ in a buffer containing \n•\n
(goto-char (match-beginning 0))
(looking-at re))))
(defun fstar--adjust-point-forward ()
"Move point forward and return a boolean indicating success."
(unless (eobp) (forward-char) t))
(defun fstar--search-predicated-forward (test-fn needle &optional bound)
"Search forward for matches of NEEDLE before BOUND satisfying TEST-FN."
(when (fstar--search-predicated #'search-forward test-fn
#'fstar--adjust-point-forward needle bound)
(goto-char (match-end 0))))
(defun fstar--re-search-predicated-forward (test-fn re &optional bound)
"Search forward for matches of RE before BOUND satisfying TEST-FN."
(when (fstar--search-predicated #'re-search-forward test-fn
#'fstar--adjust-point-forward re bound)
(goto-char (match-end 0))))
(defun fstar--adjust-point-backward ()
"Move point backward and return a boolean indicating success."
(unless (bobp) (backward-char) t))
(defun fstar--re-search-predicated-backward (test-fn re &optional bound)
"Search backwards for matches of RE before BOUND satisfying TEST-FN."
(when (fstar--search-predicated #'re-search-backward test-fn
#'fstar--adjust-point-backward re bound)
(goto-char (match-beginning 0))))
(defmacro fstar--widened (&rest body)
"Run BODY widened."
(declare (indent 0) (debug t))
`(save-restriction (widen) ,@body))
(defmacro fstar--widened-excursion (&rest body)
"Run BODY widened in a `save-excursion' block."
(declare (indent 0) (debug t))
`(save-excursion (fstar--widened ,@body)))
(defmacro fstar--prog-widened-excursion (&rest body)
"Run BODY widened in a `save-excursion' block."
(declare (indent 0) (debug t))
`(save-excursion (save-restriction (prog-widen) ,@body)))
(defun fstar--specified-space-to-align-right (str &optional padding)
"Compute a specified space to align STR PADDING spaces away from the right."
(let ((str-width (string-width str)))
(propertize " " 'display `(space :align-to (- right ,str-width ,(or padding 0))))))
(defun fstar--insert-with-face (face fmt &rest args)
"Insert (format FMT args) with FACE.
With no ARGS, just insert FMT."
(declare (indent 2))
(let ((beg (point)))
(insert (if args (apply #'format fmt args) fmt))
(font-lock-append-text-property beg (point) 'face face)))
(defun fstar--insert-ln-with-face (face fmt &rest args)
"Insert (format FMT args) with FACE and add a newline.
With no ARGS, just insert FMT and a newline."
(declare (indent 2))
(apply #'fstar--insert-with-face face fmt args)
(insert "\n"))
(defun fstar--set-text-props (str &rest props)
"Set text properties on STR to PROPS."
(set-text-properties 0 (length str) props str))
(defun fstar--truncate-left (str maxlen)
"Truncate STR from the start to MAXLEN characters."
(let* ((strlen (length str))
(ntrunc (- strlen maxlen)))
(if (<= ntrunc 0) str
(concat "…" (substring str (1+ ntrunc) strlen)))))
;;; Debugging
(define-obsolete-variable-alias 'fstar-subp-debug 'fstar-debug "0.4")
(defvar fstar-debug nil
"If non-nil, print debuging information in interactive mode.")
(define-obsolete-function-alias 'fstar-subp-toggle-debug 'fstar-toggle-debug "0.4")
(defun fstar-toggle-debug ()
"Toggle `fstar-debug'."
(interactive)
(message "F*: Debugging %s."
(if (setq-default fstar-debug (not fstar-debug)) "enabled" "disabled"))
(when fstar-debug
(display-buffer (fstar--log-buffer))))
(defconst fstar--log-buffer-keywords
'(("!!!" . font-lock-warning-face)
(">>>" . font-lock-constant-face)
("\\(;;;\\)\\(.*\\)"
(1 font-lock-constant-face prepend)
(2 font-lock-comment-face prepend))
("^#done-n?ok" . font-lock-builtin-face)))
(defun fstar--log-buffer ()
"Get or create log buffer."
(or (get-buffer "*fstar-debug*")
(with-current-buffer (get-buffer-create "*fstar-debug*")
(setq-local truncate-lines t)
(setq-local window-point-insertion-type t)
(setq-local font-lock-defaults '(fstar--log-buffer-keywords))
(font-lock-mode 1)
(buffer-disable-undo)
(current-buffer))))
(defun fstar--log (kind format &rest args)
"Log a message of kind KIND, conditional on `fstar-debug'.
FORMAT and ARGS are as in `message'.
LEVEL is one of `info', `input', `output'."
(with-current-buffer (fstar--log-buffer)
(goto-char (point-max))
(let* ((raw (apply #'format format args))
(head (cdr (assq kind '((info . ";;; ") (in . ">>> ")
(warning . "!!! ") (out . ""))))))
(fstar-assert head)
(insert (fstar--indent-str raw head)
(if (eq kind 'out) "" "\n")))))
(defmacro fstar-log (kind format &rest args)
"Log a message of kind KIND, conditional on `fstar-debug'.
FORMAT and ARGS are as in `message'."
(declare (debug t))
`(when fstar-debug
(fstar--log ,kind ,format ,@args)))
(defun fstar--write-transcript-1 (fname lines)
"Write (nreverse LINES) to FNAME."
(with-temp-buffer
(dolist (line (nreverse lines))
(insert line "\n"))
(write-region (point-min) (point-max) fname)))
(defun fstar-write-transcript-1 (prefix)
"Write latest transcript in current buffer to PREFIX.IN and PREFIX.OUT."
(interactive "FSave transcript as: ")
(goto-char (point-max))
(unless (re-search-backward "^;;; \n" nil t)
(user-error "Could not find a complete transcript to save"))
(let ((log (buffer-substring-no-properties (match-end 0) (point-max)))
(exclude-re (concat "^" (regexp-opt '(">>> " "!!! " ";;; "))))
(in nil) (out nil))
(dolist (line (delete "" (split-string log "\n")))
(cond
((string-match "^>>> " line)
(push (substring line (match-end 0)) in))
((not (string-match-p exclude-re line))
(unless (equal line "") (push line out)))))
(fstar--write-transcript-1 (concat prefix ".in") in)
(fstar--write-transcript-1 (concat prefix ".out") out)))
(defun fstar-write-transcript (prefix)
"Write latest transcript to PREFIX.IN and PREFIX.OUT."
(interactive "FSave transcript as: ")
(unless fstar-debug
(user-error "Use `fstar-toggle-debug' to collect traces"))
(with-current-buffer (fstar--log-buffer)
(fstar-write-transcript-1 prefix)))
;;; Compatibility across F* versions
(defvar-local fstar--vernum nil
"F*'s version number.")
(defcustom fstar-assumed-vernum "0.9.4.3"
"Version number to assume if F* returns an unknown version number.
This is useful when running an F#-compiled F*. Use 42.0 to
enable all experimental features."
:group 'fstar
:type 'string)
(defconst fstar--features-min-version-alist
'((absolute-linums-in-errors . "0.9.3.0-beta2")
(lookup . "0.9.4.1")
(info-includes-symbol . "0.9.4.2")
(autocomplete . "0.9.4.2")
(json-subp . "0.9.4.3")))
(defvar-local fstar--features nil
"List of available F* features.")
(defun fstar--query-vernum (executable)
"Ask F* EXECUTABLE for its version number."
(let ((fname buffer-file-name))
(with-temp-buffer
(let ((buffer-file-name fname)
(exit-code (process-file executable nil t nil "--version")))
(unless (equal exit-code 0)
(error "Failed to check F*'s version: “%s”" (buffer-string))))
(buffer-string))))
(defun fstar--init-compatibility-layer (executable)
"Adjust compatibility settings based on EXECUTABLE's version number."
(let* ((version-string (fstar--query-vernum executable)))
(if (string-match "^F\\* \\([0-9][- .[:alnum:]]*\\)" version-string)
(setq fstar--vernum (match-string 1 version-string))
(let ((print-escape-newlines t))
(message "F*: Can't parse version number from %S; assuming %s (\
don't worry about this if you're running an F#-based F* build)."
version-string fstar-assumed-vernum))
(setq fstar--vernum "unknown")))
(let ((vernum (if (equal fstar--vernum "unknown") fstar-assumed-vernum fstar--vernum)))
(pcase-dolist (`(,feature . ,min-version) fstar--features-min-version-alist)
(when (version<= min-version vernum)
(push feature fstar--features)))))
(defun fstar--has-feature (feature &optional error-fn)
"Check if FEATURE is available in the current F*.
If not, call ERROR-FN if supplied with a relevant message."
(or (null fstar--vernum)
(memq feature fstar--features)
(ignore
(and error-fn
(funcall error-fn "This feature isn't available \
in your version of F*. You're running version %s" fstar--vernum)))))
;;; Whole-buffer Flycheck
(defcustom fstar-flycheck-checker 'fstar-interactive
"Which Flycheck checker to use in F*-mode."
:group 'fstar
:type '(choice (const :tag "No Flycheck support" nil)
(const :tag "Whole-buffer verification (slow)" fstar)
(const :tag "Lightweight typechecking (fast)" fstar-interactive)))
(make-variable-buffer-local 'fstar-flycheck-checker)
(defconst fstar-error-patterns
(let* ((digits '(+ (any digit)))
(line-col `("(" line "," column "-" ,digits "," ,digits ")")))
`((error (file-name) ,@line-col ": (Error) " (message))
(warning (file-name) ,@line-col ": (Warning) " (message)))))
(defun fstar--flycheck-verify-enabled (checker)
"Create a verification result announcing whether CHECKER is enabled."
(let ((enabled (eq fstar-flycheck-checker checker)))
(list
(flycheck-verification-result-new
:label "Checker selection"
:message (if enabled "OK, checker selected"
(format "Set ‘fstar-flycheck-checker’ \
to ‘%S’ to use this checker." checker))
:face (if enabled 'success '(bold error))))))
(flycheck-define-command-checker 'fstar
"Flycheck checker for F*."
:command '("fstar.exe" source-inplace)
:error-patterns fstar-error-patterns
:error-filter #'flycheck-increment-error-columns
:modes '(fstar-mode)
:verify (apply-partially #'fstar--flycheck-verify-enabled)
:predicate (lambda () (eq fstar-flycheck-checker 'fstar)))
(add-to-list 'flycheck-checkers 'fstar)
(defun fstar-setup-flycheck ()
"Prepare Flycheck for use with F*."
(flycheck-mode 1))
;;; Prettify symbols
(defcustom fstar-symbols-alist '(("exists" . ?∃) ("forall" . ?∀) ("fun" . ?λ)
("nat" . ?ℕ) ("int" . ?ℤ)
("True" . ?⊤) ("False" . ?⊥)
("*" . ?×) (":=" . ?≔) ("::" . ?⸬)
("<=" . ?≤) (">=" . ?≥) ("<>" . ?≠)
("/\\" . ?∧) ("\\/" . ?∨) ("~" . ?¬) ("||" . ?‖)
("<==>" . ?⟺) ("==>" . ?⟹) ;; ("<==" . ?⟸)
("->" . ?→) ("~>" . ?↝) ("=>" . ?⇒)
("<-" . ?←) ("<--" . ?⟵) ("-->" . ?⟶)
("<<" . ?≪) ;; (">>" . ?≫)
("<|" . ?◃) ("|>" . ?▹) ;; ("<||" . ?⧏) ("||>" . ?⧐)
;; ("(|" . ?⦅) ("|)" . ?⦆)
("'a" . ?α) ("'b" . ?β) ("'c" . ?γ)
("'d" . ?δ) ("'e" . ?ε) ("'f" . ?φ)
("'g" . ?χ) ("'h" . ?η) ("'i" . ?ι)
("'k" . ?κ) ("'m" . ?μ) ("'n" . ?ν)
("'p" . ?π) ("'q" . ?θ) ("'r" . ?ρ)
("'s" . ?σ) ("'t" . ?τ) ("'u" . ?ψ)
("'w" . ?ω) ("'x" . ?ξ) ("'z" . ?ζ))
"F* symbols."
:group 'fstar
:type 'alist)
(defun fstar--same-ish-syntax (other ref)
"Check if the syntax class of OTHER is similar to that of REF."
(memq (char-syntax (or other ?\s))
(if (memq (char-syntax ref) '(?w ?_))
'(?w ?_)
'(?. ?\\))))
(defun fstar--prettify-symbols-predicate (start end &optional _match)
"Decide whether START..END should be prettified.
Differs from `prettify-symbols-default-compose-p' inasmuch as it
allows composition in code comments."
(and (not (fstar--same-ish-syntax (char-before start) (char-after start)))
(not (fstar--same-ish-syntax (char-after end) (char-before end)))
;; Test both endpoints because `syntax-ppss' doesn't include comment
;; openers in comments (i.e. the ‘*’ of ‘(*’ isn't in the comment).
(not (fstar--in-string-p start))
(not (fstar--in-string-p end))
(fstar--in-code-p start)
(fstar--in-code-p end)))
(defun fstar-setup-prettify ()
"Setup prettify-symbols for use with F*."
(when (and (boundp 'prettify-symbols-alist)
(fboundp 'prettify-symbols-mode))
(setq-local prettify-symbols-alist (append fstar-symbols-alist
prettify-symbols-alist))
(when (boundp 'prettify-symbols-compose-predicate)
(setq prettify-symbols-compose-predicate #'fstar--prettify-symbols-predicate))
(prettify-symbols-mode)))
;;; Font-Lock
(defconst fstar-syntax-headers
'("open" "module" "include" "friend"
"let" "let rec" "val" "and" "assume"
"exception" "effect" "new_effect" "sub_effect" "new_effect_for_free" "layered_effect"
"kind" "type" "class" "instance"))
(defconst fstar-syntax-fsdoc-keywords
'("@author" "@summary"))
(defconst fstar-syntax-fsdoc-keywords-re
(format "^[[:space:]]*\\(%s\\)\\_>" (regexp-opt fstar-syntax-fsdoc-keywords)))
(defconst fstar-syntax-preprocessor
'("#set-options" "#reset-options" "#push-options" "#pop-options" "#restart-solver" "#light"))
(defconst fstar-syntax-qualifiers
'("new" "abstract" "logic" "assume" "visible"
"unfold" "irreducible" "inline_for_extraction" "noeq" "noextract" "unopteq"
"private" "opaque" "total" "default" "reifiable" "reflectable"))
(defconst fstar-syntax-block-header-re
(format "^\\(?:%s[ \n]\\)*%s "
(regexp-opt fstar-syntax-qualifiers)
(regexp-opt fstar-syntax-headers))
"Regexp matching block headers.")
(defconst fstar-syntax-block-start-re
(format "^\\(?:%s[ \n]\\)*\\(\\[@\\|%s \\)"
(regexp-opt fstar-syntax-qualifiers)
(regexp-opt (append (remove "and" fstar-syntax-headers)
fstar-syntax-preprocessor)))
"Regexp matching starts of semantic blocks.")
(defconst fstar-syntax-block-delims
`("begin" "end" "in"))
(defconst fstar-syntax-structure-re
(regexp-opt (append fstar-syntax-block-delims
fstar-syntax-headers
fstar-syntax-qualifiers)
'symbols))
(defconst fstar-syntax-preprocessor-re
(regexp-opt fstar-syntax-preprocessor 'symbols))
(defconst fstar-syntax-keywords
'("of" "by" "normalize_term"
"forall" "exists"
"assert" "assert_norm" "assert_spinoff" "assume"
"fun" "function"
"calc"
"introduce" "eliminate" "returns"
"try" "match" "when" "with"
"if" "then" "else"
"ALL" "All" "DIV" "Div" "EXN" "Ex" "Exn" "GHOST" "GTot" "Ghost"
"Lemma" "PURE" "Pure" "Tot" "ST" "STATE" "St"
"Unsafe" "Stack" "Heap" "StackInline" "Inline"))
(defconst fstar-syntax-keywords-re
(regexp-opt fstar-syntax-keywords 'symbols))
(defconst fstar-syntax-builtins
'("requires" "ensures" "modifies" "decreases" "attributes"
"effect_actions"))
(defconst fstar-syntax-builtins-re
(regexp-opt fstar-syntax-builtins 'symbols))
(defconst fstar-syntax-ambiguous-re
(regexp-opt `("\\/" "/\\")))
(defconst fstar-syntax-constants
'("False" "True"))
(defconst fstar-syntax-constants-re
(regexp-opt fstar-syntax-constants 'symbols))
(defconst fstar-syntax-risky
'("assume" "admit" "admitP" "magic" "unsafe_coerce" "sladmit" "tadmit" "tadmit_t" "admit1" "admit_all" "admit_dump" "magic_dump"))
(defconst fstar-syntax-risky-re
(regexp-opt fstar-syntax-risky 'symbols))
(defconst fstar-syntax-reserved-exact-re
(format "\\`%s\\'"
(regexp-opt
(append `("Type")
fstar-syntax-headers
fstar-syntax-fsdoc-keywords
fstar-syntax-preprocessor
fstar-syntax-qualifiers
fstar-syntax-block-delims
fstar-syntax-keywords
fstar-syntax-builtins
fstar-syntax-constants))))
(defface fstar-structure-face
'((((background light)) (:bold t))
(t :bold t :foreground "salmon"))
"Face used to highlight structural keywords."
:group 'fstar)
(defface fstar-risky-face
'((t :underline t :inherit font-lock-warning-face))
"Face used to highlight risky keywords (‘admit’ etc.)."
:group 'fstar)
(defface fstar-subtype-face
'((t :slant italic))
"Face used to highlight subtyping clauses."
:group 'fstar)
(defface fstar-attribute-face
'((t :slant italic))
"Face used to highlight attributes."
:group 'fstar)
(defface fstar-decreases-face
'((t :slant italic))
"Face used to highlight decreases clauses."
:group 'fstar)
(defface fstar-subscript-face
'((t :height 1.0))
"Face used to highlight subscripts"
:group 'fstar)
(defface fstar-braces-face
'((t))
"Face to use for { and }."
:group 'fstar)
(defface fstar-dereference-face
'((t :inherit font-lock-negation-char-face))
"Face to use for !."
:group 'fstar)
(defface fstar-ambiguous-face
'((t :inherit font-lock-negation-char-face))
"Face to use for /\\ and \\/."
:group 'fstar)
(defface fstar-universe-face
'((t :foreground "forest green"))