Skip to content

Commit 6acb3cb

Browse files
authored
Merge pull request #93 from kape1395/lsp
Language Server Protocol for TLAPM
2 parents 494f83b + 4d53b9e commit 6acb3cb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+5157
-96
lines changed

.github/workflows/ci.yml

+7-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- macos-13
1717
- macos-14
1818
ocaml-compiler:
19-
- '4.13.1'
19+
- '4.14.1'
2020
- '5.1.0'
2121
env:
2222
EXAMPLES_DIR: "tlaplus-examples"
@@ -39,10 +39,15 @@ jobs:
3939
with:
4040
path: _build_cache
4141
key: ${{ runner.os }}_build_cache
42+
- name: Install optional dependencies
43+
if: startsWith(matrix.ocaml-compiler, '5.')
44+
run: |
45+
eval $(opam env)
46+
make opam-deps-opt
4247
- name: Build TLAPM
4348
run: |
4449
eval $(opam env)
45-
opam install ./ --deps-only --yes
50+
make opam-deps
4651
make
4752
make release
4853
- name: Run tests

.github/workflows/release.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,15 @@ jobs:
9797
with:
9898
path: _build_cache
9999
key: ${{ runner.os }}_build_cache
100+
- name: Install optional dependencies
101+
if: startsWith(matrix.ocaml-compiler, '5.')
102+
run: |
103+
eval $(opam env)
104+
make opam-deps-opt
100105
- name: Build installer of TLAPS
101106
run: |
102107
eval $(opam env)
103-
opam install ./ --deps-only --yes
108+
make opam-deps
104109
make
105110
make release
106111
echo "TLAPM_RELEASE_FILE=$(make release-print-file)" >> "$GITHUB_ENV"

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99

1010
/_build/
1111
/_build_cache/
12-
/.vscode/
12+
/.vscode/settings.json
1313
/tlaps-*.tar.gz
14+
/src/tlapm.bc
1415

1516
__pycache__/
1617
*.pyc

.ocamlformat

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
version=0.26.2
2+
profile=default

.vscode/cspell.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"words": [
3+
"tlaplus",
4+
"tlaps",
5+
"tlapm",
6+
"zenon",
7+
"opam",
8+
"ocaml",
9+
"caml",
10+
"sandboxing",
11+
"sprintf",
12+
"printexc"
13+
]
14+
}

Makefile

+8-1
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ opam-update: # Update the package lists and install updates.
2424
opam-deps:
2525
opam install ./ --deps-only --yes --working-dir
2626

27+
opam-deps-opt:
28+
opam install --yes eio_main lsp
29+
2730
opam-deps-dev:
28-
opam install ocamlformat ocaml-lsp-server earlybird
31+
opam install --yes ocamlformat ocaml-lsp-server earlybird
2932

3033
build:
3134
dune build
@@ -44,6 +47,10 @@ test-fast:
4447
test-fast-basic:
4548
make -C test fast/basic
4649

50+
fmt:
51+
# Only the LSP part is not formatted automatically.
52+
cd lsp && dune fmt
53+
4754
install:
4855
dune install --prefix=$(PREFIX)
4956
make -C $(PREFIX)/lib/tlapm/ -f Makefile.post-install

dune-project

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
(license BSD-2-Clause)
1414

15+
(cram enable)
16+
1517
(package
1618
(name tlapm)
1719
(synopsis "TLA+ Proof Manager")
@@ -42,5 +44,11 @@
4244
dune-site
4345
dune-build-info
4446
sexplib
47+
cmdliner
48+
camlzip
49+
re2
4550
ppx_inline_test
46-
ppx_assert))
51+
ppx_assert)
52+
(depopts
53+
lsp ; https://github.com/ocaml/ocaml-lsp
54+
eio_main)) ; https://github.com/ocaml-multicore/eio, only available on OCaml version >= 5.

library/dune

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
(install
2-
(section (site (tlapm stdlib)))
3-
(files (glob_files "*.tla")))
2+
(section
3+
(site
4+
(tlapm stdlib)))
5+
(files
6+
(glob_files "*.tla")))

lsp/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# LSP interface to the TLAPS
2+
3+
The `tlapm_lsp` works as an adapter between the `tlapm` and code editors, e.g., VSCode.
4+
It is responsible for:
5+
- Parsing the document structure to locate features related to proofs, e.g.:
6+
- proof obligation by cursor position, possibly collecting all the subtree of obligations.
7+
- Running the `tlapm` on specific obligations and reporting the results.
8+
- Show progress of the tlapm, allow cancel it.
9+
- TODO: Proof re-numbering.
10+
- TODO: Proof decomposition.
11+
- TODO: Easier experimentation on missing facts.
12+
13+
The test folder contains a VSCode extension / LSP client. It is meant only for testing,
14+
the real integration should be done in the TLA+ VSCode extension.
15+
16+
See:
17+
- Dafny plugin. It shows all the obligations as testcases.
18+
<https://github.com/dafny-lang/ide-vscode>
19+
<https://github.com/dafny-lang/dafny/tree/master/Source/DafnyLanguageServer>
20+
- VSCode Decorators: https://github.com/microsoft/vscode-extension-samples/blob/main/decorator-sample/README.md
21+
https://vscode.rocks/decorations/
22+
Gutter -- the left side of the editor.
23+

lsp/bin/dune

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
(executable
2+
(name tlapm_lsp)
3+
(public_name tlapm_lsp)
4+
(optional) ; Only build, if eio is available, which is only the case for ocaml > 5.
5+
(enabled_if
6+
(>= %{ocaml_version}, "5.0.0"))
7+
(libraries tlapm_lsp_lib eio_main cmdliner))

lsp/bin/tlapm_lsp.ml

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
(* cSpell:words cmdliner signum Open_wronly Open_creat fprintf kdprintf *)
2+
3+
open Cmdliner
4+
5+
let traceln_mutex = Mutex.create ()
6+
7+
let transport_of_args tr_stdio tr_socket =
8+
let open Tlapm_lsp_lib.Server in
9+
match (tr_stdio, tr_socket) with
10+
| true, None -> Ok Stdio
11+
| false, Some port -> Ok (Socket port)
12+
| _ -> Error "Exactly one of transports has to be specified."
13+
14+
let run transport log_to log_io =
15+
Printexc.record_backtrace true;
16+
let main_fun (env : Eio_unix.Stdenv.base) =
17+
let main_switch _sw =
18+
let stop_promise, stop_resolver = Eio.Std.Promise.create () in
19+
let handle_signal (_signum : int) =
20+
Eio.Std.Promise.resolve stop_resolver "Stopping on SigINT"
21+
in
22+
Sys.set_signal Sys.sigint (Signal_handle handle_signal);
23+
Tlapm_lsp_lib.Server.run transport log_io env stop_promise
24+
in
25+
let with_log_stderr () = Eio.Switch.run main_switch in
26+
let with_log_file log_file =
27+
let with_log_chan log_chan =
28+
(* This is mostly a copy of default_traceln from eio/core/debug.ml,
29+
just modified to take a specific out channel instead of stderr. *)
30+
let traceln_impl ?__POS__:pos fmt =
31+
let k go =
32+
let b = Buffer.create 512 in
33+
let f = Format.formatter_of_buffer b in
34+
go f;
35+
Option.iter
36+
(fun (file, line, _, _) -> Format.fprintf f " [%s:%d]" file line)
37+
pos;
38+
Format.pp_close_box f ();
39+
Format.pp_print_flush f ();
40+
let msg = Buffer.contents b in
41+
let lines = String.split_on_char '\n' msg in
42+
Mutex.lock traceln_mutex;
43+
Fun.protect ~finally:(fun () -> Mutex.unlock traceln_mutex)
44+
@@ fun () ->
45+
List.iter (Printf.fprintf log_chan "+%s\n") lines;
46+
flush log_chan
47+
in
48+
Format.kdprintf k ("@[" ^^ fmt)
49+
in
50+
let traceln_bnd = { Eio.Debug.traceln = traceln_impl } in
51+
let debug = Eio.Stdenv.debug env in
52+
Eio.Fiber.with_binding debug#traceln traceln_bnd (fun _ ->
53+
Format.pp_set_formatter_out_channel Format.err_formatter log_chan;
54+
Eio.Switch.run main_switch)
55+
in
56+
Out_channel.with_open_gen
57+
[ Open_append; Open_wronly; Open_creat ]
58+
0o644 log_file with_log_chan
59+
in
60+
match log_to with
61+
| None -> with_log_stderr ()
62+
| Some log_file -> with_log_file log_file
63+
in
64+
Eio_main.run main_fun
65+
66+
module Cli = struct
67+
let arg_stdio =
68+
let doc = "Run LSP over StdIO." in
69+
let info = Arg.info [ "stdio" ] ~docv:"BOOL" ~doc in
70+
Arg.value (Arg.flag info)
71+
72+
let arg_socket =
73+
let doc = "Run LSP over TCP, use the specified port." in
74+
let info = Arg.info [ "socket"; "port" ] ~docv:"NUM" ~doc in
75+
Arg.value (Arg.opt (Arg.some Arg.int) None info)
76+
77+
let arg_log_to =
78+
let doc = "Log all to the specified file instead of StdErr." in
79+
let info = Arg.info [ "log-to" ] ~docv:"FILE" ~doc in
80+
Arg.value (Arg.opt (Arg.some Arg.string) None info)
81+
82+
let arg_log_io =
83+
let doc = "Log protocol's IO." in
84+
let info = Arg.info [ "log-io" ] ~docv:"BOOL" ~doc in
85+
Arg.value (Arg.flag info)
86+
87+
let term () =
88+
let combine tr_stdio tr_socket log_to log_io =
89+
match transport_of_args tr_stdio tr_socket with
90+
| Ok transport -> `Ok (run transport log_to log_io)
91+
| Error err -> `Error (false, err)
92+
in
93+
Term.(const combine $ arg_stdio $ arg_socket $ arg_log_to $ arg_log_io)
94+
95+
let name = "tlapm_lsp"
96+
let doc = "LSP interface for TLAPS."
97+
98+
let man =
99+
[
100+
`S Manpage.s_description;
101+
`P "tlapm_lsp allows LSP based IDEs to access the prover functions.";
102+
`S Manpage.s_see_also;
103+
`P "The TLAPM code repository: https://github.com/tlaplus/tlapm";
104+
]
105+
106+
let main () =
107+
let info = Cmd.info ~doc ~man name in
108+
Cmd.v info (Term.ret (term ())) |> Cmd.eval |> Stdlib.exit
109+
end
110+
111+
let () = Cli.main ()

lsp/bin/tlapm_lsp.mli

Whitespace-only changes.

lsp/components.puml

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
@startuml TLAPM-LSP
2+
3+
component LspServer
4+
LspServer --> Session
5+
6+
note as N_General
7+
General layout of the tlapm_lsp_lib.
8+
end note
9+
10+
component Session as Session {
11+
component Handler
12+
}
13+
Session --> Prover
14+
Session --> Docs
15+
16+
component Docs {
17+
component Doc
18+
component Version
19+
component Actual
20+
Doc *-- Version
21+
Doc *-- Actual
22+
Actual --> Parser
23+
}
24+
component Parser {
25+
component TLAPM
26+
component SANY
27+
}
28+
component Prover {
29+
component Runner
30+
component Toolbox
31+
component Progress
32+
Runner --> Toolbox
33+
Runner --> Progress
34+
}
35+
@enduml

lsp/lib/const.ml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let diagnostic_source = "TLAPM"

lsp/lib/const.mli

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
val diagnostic_source : string

lsp/lib/docs/doc.ml

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
open Util
2+
3+
(* Max number of unprocessed/pending versions to keep. *)
4+
let keep_vsn_count = 50
5+
6+
type t = {
7+
uri : LspT.DocumentUri.t;
8+
pending : Doc_vsn.t list;
9+
(** All the received but not yet processed versions. *)
10+
actual : Doc_actual.t; (** Already processed version. *)
11+
}
12+
13+
let make uri tv parser =
14+
{ uri; pending = []; actual = Doc_actual.make uri tv None parser }
15+
16+
let with_parser doc parser =
17+
{ doc with actual = Doc_actual.with_parser doc.actual parser }
18+
19+
let add doc tv =
20+
let drop_till = Doc_vsn.version tv - keep_vsn_count in
21+
let drop_unused = List.filter (fun pv -> Doc_vsn.version pv < drop_till) in
22+
{ doc with pending = tv :: drop_unused doc.pending }
23+
24+
let latest_vsn doc =
25+
match doc.pending with
26+
| [] -> Doc_actual.vsn doc.actual
27+
| latest :: _ -> Doc_vsn.version latest
28+
29+
let set_actual_vsn doc vsn =
30+
if Doc_actual.vsn doc.actual = vsn then Some doc
31+
else
32+
let rec drop_after_vsn = function
33+
| [] -> (None, [])
34+
| (pv : Doc_vsn.t) :: pvs ->
35+
if Doc_vsn.version pv = vsn then (Some pv, [])
36+
else
37+
let res, pvs = drop_after_vsn pvs in
38+
(res, pv :: pvs)
39+
in
40+
let selected, pending = drop_after_vsn doc.pending in
41+
match selected with
42+
| None -> None
43+
| Some selected ->
44+
let actual =
45+
Doc_actual.make doc.uri selected (Some doc.actual)
46+
(Doc_actual.parser doc.actual)
47+
in
48+
Some { doc with actual; pending }
49+
50+
let with_actual doc f =
51+
let doc, act, res = f doc doc.actual in
52+
let doc = { doc with actual = act } in
53+
(doc, res)

lsp/lib/docs/doc.mli

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
(** Represents a document identified by its uri. It can contain multiple versions and all the related info. *)
2+
3+
open Util
4+
5+
type t
6+
7+
val make : LspT.DocumentUri.t -> Doc_vsn.t -> Util.parser_fun -> t
8+
val with_parser : t -> Util.parser_fun -> t
9+
val add : t -> Doc_vsn.t -> t
10+
val latest_vsn : t -> int
11+
val set_actual_vsn : t -> int -> t option
12+
val with_actual : t -> (t -> Doc_actual.t -> t * Doc_actual.t * 'a) -> t * 'a

0 commit comments

Comments
 (0)