Skip to content

fix(lsp): show success hints on the introducing keyword of commands and tactics#1444

Open
ciaran-matthew-dunne wants to merge 4 commits into
Deducteam:masterfrom
ciaran-matthew-dunne:pr/lsp-success-hints
Open

fix(lsp): show success hints on the introducing keyword of commands and tactics#1444
ciaran-matthew-dunne wants to merge 4 commits into
Deducteam:masterfrom
ciaran-matthew-dunne:pr/lsp-success-hints

Conversation

@ciaran-matthew-dunne

@ciaran-matthew-dunne ciaran-matthew-dunne commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1450 (bare open parsing fix): the first commit in the diff belongs to that PR; review the last three here.

The LSP server emits severity-4 "OK" diagnostics for every successfully checked command and tactic. These previously spanned the whole command or tactic, so the success underline covered symbol bodies, entire proofs and rule right-hand sides. With this PR the hint is shown on the introducing keyword only (symbol, rule, require, …; assume, rewrite, …).

Notes:

  • For most commands the keyword position is computed as a prefix of the command's position (Pos.prefix). This is not possible for symbol (modifiers may precede the keyword), inductive (modifiers and parameters may) and open (a private modifier may), so for these three constructors the parser records the keyword position in the AST. Syntax.command_keyword_pos handles all cases, and falls back to the whole command's position when no keyword position was recorded (Dedukti input). require needs no recorded position: with fix parsing of bare open: do not load modules as require open does #1450 it is always the first keyword of its command.
  • Tactic hints keep the start of their position unchanged: the same (position, goals) records anchor the goals panel, and its lookup (closest_before) only reads position starts. Only the end of the range shrinks, so proof/goals answers are unchanged. The keyword is used rather than the tactic's end because the end is ill-defined once a tactic has subproofs.
  • Symbol declarations always create proof data, so their "OK" goes through the proof path. The initial goals stay anchored at the symbol name (the goals panel shows the initial proof state there); the visible hint goes on the symbol keyword.
  • The keyword tables (tactic_keyword, query_keyword) have no wildcard case, so adding a constructor is a compile error until they are updated.
  • Error diagnostics are unchanged: they still span the whole command or tactic.
  • Also renames the lp_doc node field ast to cmd.

@ciaran-matthew-dunne ciaran-matthew-dunne left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

@ciaran-matthew-dunne ciaran-matthew-dunne marked this pull request as ready for review July 6, 2026 20:10
ciaran-matthew-dunne added a commit to ciaran-matthew-dunne/lambdapi that referenced this pull request Jul 6, 2026
The test harness was written against the old bundled LSP branch. The
fixes split out into Deducteam#1366 (file:// URIs in definition responses,
qualified-name resolution) and Deducteam#1444 (keyword-anchored OK hints) are
not on this branch, so the cases asserting them are marked
expectedFailure, each tagged with the PR it waits for. When those PRs
merge and this branch rebases, they flip to unexpected-success and
the decorators come off.
ciaran-matthew-dunne added a commit to ciaran-matthew-dunne/lambdapi that referenced this pull request Jul 6, 2026
Python harness driving the server over stdio (make test_lsp):
lifecycle, diagnostics, definition, hover, goals, symbols,
completion, debouncing. Five cases assert behavior from Deducteam#1366 and
Deducteam#1444 and are marked expectedFailure until those PRs merge.
Comment thread src/pure/pure.ml Outdated
Comment thread src/pure/pure.ml Outdated
Comment thread src/lsp/lp_doc.ml Outdated
Comment thread src/lsp/lp_doc.ml Outdated
@ciaran-matthew-dunne ciaran-matthew-dunne force-pushed the pr/lsp-success-hints branch 4 times, most recently from add4a14 to e8a35a8 Compare July 8, 2026 13:46
ciaran-matthew-dunne added a commit to ciaran-matthew-dunne/lambdapi that referenced this pull request Jul 8, 2026
Python harness driving the server over stdio (make test_lsp):
lifecycle, diagnostics, definition, hover, goals, symbols,
completion, debouncing. Five cases assert behavior from Deducteam#1366 and
Deducteam#1444 and are marked expectedFailure until those PRs merge.
@ciaran-matthew-dunne ciaran-matthew-dunne changed the title fix(lsp): attach success hints to the leading keyword, not the whole command fix(lsp): show success hints on a command's ";" and a tactic's keyword Jul 8, 2026
Comment thread src/lsp/lp_doc.ml Outdated
| None -> None
| Some p ->
Pos.(Some { p with start_line = p.end_line; start_col = p.end_col + 1
; end_line = p.end_line; end_col = p.end_col + 2 })

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several problems here:

  • start_offset and end_offset should be set properly as well

  • Why not taking end_col = p.end_col + 1 (";" is one character long only)?

  • But this position is wrong if the semicolon is at the beginning of the line as in:

command ...
;

In this case, we should use {p with start_line = p.end_line; start_col = 0; end_col = 0} instead.
Problem:

command ...
 ; // semicolon with one space before

gives the same position as the previous case because of the current definition of extend_pos which extends a position until the previous column of the next token if it is >0.

So we should instead do:

let at_semicolon (pos:Pos.popt): Pos.popt =
  match pos with
  | None -> None
  | Some p ->
    if p.end_col = 0 then {p with start_offset=p.end_offset; start_line=p.end_line; start_col=0;
                                                        end_offset=p.end_offset+1; end_col=1}
      (* in this case we don't know whether ";" is at position 0 or 1, so we include both columns *)
    else {p with start_offset=p.end_offset+1; start_line=p.end_line; start_col=p.end_col+1;
                            end_offset=P.end_offset+1; end_col=p.end_col+1}

To be fully precise and not include a useless space, we need to include ";" in the position of a command.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait. The problem with the position of the trailing ";" comes from line 1719 of lpParser.ml. Instead of
| SEMICOLON -> c
one should do
| SEMICOLON -> extend_pos lb ... c

I think that that should fix the issue(*)

(*) In truth, the fact that the "command" parser is not allowed to consume the terminator is just plain weird and a symptom of bad coding elsewhere. The point is that in pure.ml (if I remember correctly) every time a new command is to be parsed, the lexbuf at the char Stream.t level is passed again. Instead the lexbuf at the lookahead level (the one I implemented) should be passed around. Right now the main parser functions exported by parser take in input the lexbuf at char level and build on-the-fly a new lexbuf at the backtracking level. Instead one should export a function to turn a lexbuf into an higher level lexbuf and then pass that again and again. Or, a less impacting alternative on the code, the parse_lexbuf function in parsing should take the low level lexbuf and return a closure that goes from unit to command, remembering the high level lexbuf inside the closure. Then pure.ml could just iterate this closure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh well, I decided that the diagnostic should sit on the keyword of the command anyway.

e.g., the diagnostic for the command constant symbol foo : TYPE; is placed on symbol,
and similarly the diagnostic for [a : Set] inductive T : TYPE ≔ ... is placed on inductive.

Frédéric talked to me this morning about the possibility of retiring the use of the semicolon in lambdapi. So hopefully these changes in this PR are compatible with any future changes to the parser.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix at line 1719 I proposed should be implemented anyway since it is nevertheless a bug IMHO. About the idea of retiring the ; terminator: in that case it will be MANDATORY to also apply the long explanation improvement I described, because otherwise with no semicolon to stop at one character will be definitely lost at the next call.

Comment thread src/lsp/lp_doc.ml Outdated
| None -> None
| Some p ->
Pos.(Some { p with end_line = p.start_line
; end_col = p.start_col + String.length kw })

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

end_offset should be set properly as well, I believe to the same value as for end_col

@ciaran-matthew-dunne ciaran-matthew-dunne changed the title fix(lsp): show success hints on a command's ";" and a tactic's keyword fix(lsp): show success hints on the introducing keyword of commands and tactics Jul 9, 2026
A bare open (not preceded by require) was parsed as require open
since the LL(1) parser rework (Deducteam#1441), silently loading not yet
required modules instead of failing, and pretty-printing back as
require open.
The severity-4 "OK" diagnostic of a command or tactic previously spanned
the whole command or tactic, so the success underline covered symbol
bodies, rule right-hand sides and whole proofs. Instead:

- A command's hint is shown on its terminating ";". The command's
  recorded position stops one character before it (the parser's
  extend_pos), so lp_doc derives the ";" position from it (at_semicolon).

- Symbol declarations always create proof data, so their "OK" went
  through the proof path and landed on the symbol name. The initial
  goals stay anchored at the symbol (for the goals panel), but the
  visible hint now goes on the ";".

- A tactic's hint is shown on its leading keyword (Syntax.tactic_keyword,
  exposed as Pure.Tactic.keyword), since a tactic's end is ill-defined
  once it has subproofs. These diagnostics also anchor the goals panel,
  whose lookup (closest_before) only reads the start of each position,
  which does not move.

Also rename the lp_doc node field "ast" to "cmd".
Set the character offsets of hint positions consistently with their
columns (at_keyword left end_offset at the end of the whole tactic,
at_semicolon did not update offsets at all).

Handle a ";" at the beginning of a line: the parser's extend_pos does
not back up at column 0, so a command position with end_col = 0 cannot
distinguish a ";" at column 0 from one at column 1 (the hint used to
underline column 1, one character past a line-initial ";"). Cover both
columns in that case.
Show the "OK" hint of a command on its keyword ("symbol", "rule",
"require", ...) rather than on the terminating ";".

The keyword of a symbol, inductive or open command is not always at
the command's start (modifiers or parameters may precede it), so the
parser records the keyword position in the AST for these three
constructors. For the other commands, Syntax.command_keyword_pos
computes it as a prefix of the command's position. Tactics use the
same mechanism (tactic_keyword_pos); at_keyword and at_semicolon
disappear from lp_doc, replaced by Pos.prefix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants