Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AST exploration features for ppx developers. #666

Closed
wants to merge 168 commits into from

Conversation

arozovyk
Copy link
Contributor

@arozovyk arozovyk commented Jul 19, 2021

Screenshot 2021-07-19 at 18 02 57

This PR adds a set of features aimed at simplifying the process of developing (and using) OCaml preprocessors.
Namely,

Exploring the Abstract Syntax Tree

  • Live visualization of the OCaml AST inside a separate editor tab.
  • The means of exploring the AST both ways: from standard text editor, choosing a piece of code and revealing the corresponding AST node inside the custom UI; Highlighting the corresponding source code when mouse cursor is moved onto a node inside the UI.

This is done by using Custom Editor API that allows defining an arbitrary output for a certain type of files. Under the hood it uses the Webview, that can render (almost) any custom HTML, and it communicates with the extension via message passing.

For the UI itself, i adapted the popular open source option - astexplorer.net. Notably, besides other minor modifications (redefining the communications, adding a separate tab for preprocessed AST), I reduced the application to only contain the Tree view, which is the AST output part.

Working with preprocessed code and its AST.

  • Preview of the source code after it has been transformed by a preprocessor.
  • A separate mode to navigate the transformed AST (keeping track of the original code).

Since modern ppxes are (and should be) developed using ppxlib, this PR introduces it as a dependency. I use ppxlib for almost all AST operations, and most importantly for its version of AST, because it's the one that will also be used by ppx developers.

However, this PR uses some internal features of ppxlib that are not currently exposed so it also depends on this issue being resolved.
Meanwhile, in order to test it, one can simply use my fork of ppxlib in his local switch:

opam pin ppxlib.0.22.0 git@github.com:arozovyk/ppxlib.git

Then build astexplorer with make view-build followed by make install to install the extension.

Usage:

  1. To open AST explorer to the side: Command Palette -> OCaml: AST preview.
  2. When astexplorer is visible, click on the code you want to reveal then either Shift+Ctrl+D or right click -> Reveal AST Node to highlight and focus the node in the AST explorer.
  3. Another option to reveal AST nodes without executing a command is to activate the "Hover mode" with Shift+Ctrl+H (same to deactivate) which will make hovering over the code reveal corresponding nodes.
  4. When moving your mouse cursor inside the AST explorer, the code is automatically highlighted.
  5. [Ppx] To view the code after preprocessing by ppx: Command Palette -> Show Preprocessed Document.
  6. [Ppx] To open both preprocessed code preview and AST explorer to the side: Command Palette -> Open both ppx editor and AST exploring.
  7. [ppx] To explore the preprocessed AST, execute (6.) and choose the Preprocessed tab inside the AST explorer. The navigation is analogical to 2.-4.

The association with .ml files allows opening them with newly registrered custom editor. This is currently done by right clicking on the tab icon containing the file name and choosing Reopen Editor With from dropdown menu.
The project has to be built separately. do yarn install to get dependencies, then yarn build
From this point on, we start working with OCaml AST, and since we use ppxlibs version, we introduce it as a dependency
This function, nderived from AST type definitions by ppx_traverse (internal to ppxlib) misses some info like record names. I also add some type information in order to make things clear. In future, this function should be upstreamed to ppxlib
Usage: put typing cursor on the code you want to reveal, press Shift+Ctrl+D, or Rightclick and choose Reveal Ast Node from dropdown menu. P.S. It is this way because VSCode doesn't allow redefining left click events
Since we can't redefine event handlers for left click, the alternative is to reveal the node on hover w(by capturing mouse position, which is pretty hackish).
Desactivated by default, the mode can be enabled with Shift+Ctrl+D. This mode is here to allow users quickly navigate between nodes but also be able to desactivate it since revealing AST nodes this way might feel pretty involuntary
This part of API allows opening read-only editors with custom content
With single locations that came out from reparsing the pretty printed structure fromm pp.ml file
Meaning preprocessed code preview followed by astexplorer
The associatation is between orginal text document and the one shown in the preprocessed preview
The prompt appears when we focus the preprocessed code preview and the original document has changed. In this case we can rebuild the project and update the preview
Also close previews if the original document has been closed
Signed-off-by: Rudi Grinberg <me@rgrinberg.com>
| Error m -> raise (User_error m)
in
let lex = Lexing.from_string pp_code in
Result.ok_or_failwith
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this failwith? Isn't this an error that should be displayed in the WebView? (parsing error of sorts).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed it to managing Error (which should never occur since all of the Traverse_ast2.lift2methods returning it are accounted for by with_reparse (except if I forgot something).

| Some webview -> on_hover document webview
| None ->
show_message `Error "Webview wasn't found while switching hover mode";
failwith "Webview wasn't found while switching hover mode"
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a fatal error that the user isn't responsible for and we cannot recover from, right? In that case we should introduce an exception type called Code_error. In general, we shouldn't be using failwith at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not a fatal error, it just means that user tried to activate hover mode without opening an AST editor. The failwith is useless though 8e2c305

(TextDocument.uri document)
with
| Some x -> x
| None -> failwith "Failed finding the original document URI."
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, this exception needs to be something else:

  • User_error if the user is responsible or is part of the normal workflow
  • Code_error if it's a bug in our extension or we don't know what happened.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Again, replaced failwith with just an Error. 064c5ab

let pp_json =
let pp_code =
match fetch_pp_code ~document with
| Ok s -> s
Copy link
Contributor

Choose a reason for hiding this comment

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

When s = "" as a result of not being able to parse the file, why are we trying to parse it again?

I think fetch_pp_code should return a more compound type and let the callers report errors.

val fetch_pp_code : TextDocument.t -> (string, Ppx_tools.read_error) result

This will also prevent some errors being displayed both in the webview and in the popup.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like the read_error type isn't useful anymore. As far as I can tell, Io_error and Read_error are now handled exactly the same way. So we can drop the distinction if that's intended.

@rgrinberg
Copy link
Contributor

Final complaint about the UI:

The Retry/Abandon dialog seems somewhat suboptimal to me. If we had a refresh button/keybinding/action inside the web view that allowed us to refresh the view, that would be more than sufficient. This is less intrusive as it doesn't force the user's attention on this problem, and it also allows the user to restore the webview even if the menu was accidentally dismissed.

Copy link
Contributor Author

@arozovyk arozovyk left a comment

Choose a reason for hiding this comment

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

I'm changing this 9ba660b to a result type since we don't know where to catch exceptions in an elegant way. ( transform_to_ast being a different story).

@rgrinberg
Copy link
Contributor

I'm changing this 9ba660b to a result type since we don't know where to catch exceptions in an elegant way. ( transform_to_ast being a different story).

Sounds good

@rgrinberg
Copy link
Contributor

Let me clarify the general strategy behind error handling. In hindsight, I'm giving out concrete review feedback that is piecemeal. I can see how it might be hard to understand what is the end state that I'm looking for.

  1. Whenever we want to abort and give control back to the user, we should use User_error. The message in this exception should clear. Thus including prefixes like "Read_error: " isn't good because it is making it cryptic for the user. If something is invalid (e.g. filename), we should include the invalid data in the error message.

  2. Whenever there's an error condition that we must handle differently depending on the caller, we should prefer result types (if possible). The type signature makes it easy to remember that the error case must be explicitly handled.

  3. If our error handling code is different based on the kind of error, we should introduce a type to represent the various error conditions we intend to handle. If our error handling code is the same for different errors, this excessive level of detail is confusing and creates opportunities for bugs. We aren't writing library code, so we don't need to write fully general code.

Some consequences of the above are:

  1. We never use failwith. Because it is not clear whether the error message is a bug in our code or intended for the user.

  2. We should usually not catch User_error unless we are in the top level handler. As this error signifies that whatever operation that is being done is aborted.

Let me know if you have any questions. Thanks for being patient and polishing the code.

@arozovyk
Copy link
Contributor Author

Final complaint about the UI:

The Retry/Abandon dialog seems somewhat suboptimal to me. If we had a refresh button/keybinding/action inside the web view that allowed us to refresh the view, that would be more than sufficient. This is less intrusive as it doesn't force the user's attention on this problem, and it also allows the user to restore the webview even if the menu was accidentally dismissed.

I agree that dialog shouldn't be the only way to update the Webview, so i added a button for Preprocessed mode. I could remove the prompt but It would mean either

  1. User has to close and reopen the Preprocessed Document to update it.
  2. We gotta have an additional command to reload it.

@arozovyk
Copy link
Contributor Author

Let me clarify the general strategy behind error handling. In hindsight, I'm giving out concrete review feedback that is piecemeal. I can see how it might be hard to understand what is the end state that I'm looking for.

  1. Whenever we want to abort and give control back to the user, we should use User_error. The message in this exception should clear. Thus including prefixes like "Read_error: " isn't good because it is making it cryptic for the user. If something is invalid (e.g. filename), we should include the invalid data in the error message.
  2. Whenever there's an error condition that we must handle differently depending on the caller, we should prefer result types (if possible). The type signature makes it easy to remember that the error case must be explicitly handled.
  3. If our error handling code is different based on the kind of error, we should introduce a type to represent the various error conditions we intend to handle. If our error handling code is the same for different errors, this excessive level of detail is confusing and creates opportunities for bugs. We aren't writing library code, so we don't need to write fully general code.

Some consequences of the above are:

  1. We never use failwith. Because it is not clear whether the error message is a bug in our code or intended for the user.
  2. We should usually not catch User_error unless we are in the top level handler. As this error signifies that whatever operation that is being done is aborted.

Let me know if you have any questions. Thanks for being patient and polishing the code.

Thanks for taking your time and helping me figure this out.

I agree with the points you are making, the only thing that seems kinda wierd is why we don't catch User_error. Because if we don't, the user is never informed about it (unless he opens Dev tools). Is that what we are looking for?

@rgrinberg
Copy link
Contributor

I agree with the points you are making, the only thing that seems kinda wierd is why we don't catch User_error. Because if we don't, the user is never informed about it (unless he opens Dev tools). Is that what we are looking for?

I was looking for a good way to handle these errors, but it looks like it is currently not possible to install a handler in one place: microsoft/vscode#45264

What we can do is a workaround like in this extension: julia-vscode/julia-vscode#1276.

So I suppose that means we need to:

  • Add function that will install an exception handler (.catch) to handle User_error before ignoring
  • Add try/catch in all providers etc. to catch synchronously thrown User_error and handle them the same way as above.

Failing to update/open Preprocessed document has to be handled so it we keep the result type
@rgrinberg
Copy link
Contributor

I merged manually after squashing. Thanks for all the work.

@rgrinberg rgrinberg closed this Aug 25, 2021
@smorimoto
Copy link
Collaborator

There is no change log entry for this yet, so I think we can release 1.9.0 after adding it.

@ulugbekna
Copy link
Collaborator

ulugbekna commented Sep 5, 2021 via email

@ulugbekna
Copy link
Collaborator

Hi, I'm getting an unbound module error when I try to build the master, which version of ppxlib are we expected to have? both ppxlib master and latest release are failing

dune build src/vscode_ocaml_platform.bc.js
File "src/ppx_tools/ppx_tools.mli", line 5, characters 38-53:
5 | val get_preprocessed_ast : string -> (Ppxlib.Ast_io.t, string) result
                                          ^^^^^^^^^^^^^^^
Error: Unbound module Ppxlib.Ast_io
make: *** [build] Error 1

@rgrinberg
Copy link
Contributor

We shouldn't release before we add more principal handling of ocaml-lsp
versions in the extension; otherwise, we get false warnings about outdated
ocaml-lsp version because people on ocaml v < 4.12 cannot really update to
the latest ocaml-lsp

How about we just make it possible for the user to disable such warnings per project? I'd rather not introduce some heavy weight solution to a problem that is fixed by upgrading.

Another thing that should be handled before a release
is that jumping between holes keyboard shortcuts coincide with important
shortcuts on French keyboard.

Okay, let's get rid of the shortcuts for now then. We can think of better shortcuts later.

let relative_document_path ~document =
Workspace.asRelativePath ~pathOrUri:(`Uri (TextDocument.uri document)) ()

let project_root_path () = Workspace.rootPath ()
Copy link
Contributor

Choose a reason for hiding this comment

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

would it be possible to use dune to get the root of the project instead of vscode's workspace? The current code doesn't work in the context of a monorepo

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, but why not? Doesn't it make sense to open the monorepo such that its root matches the workspace root?

Copy link
Contributor

Choose a reason for hiding this comment

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

here I'm thinking about a monorepo in a larger sense than just a dune monorepo. A repo containing multiple projects written in multiple languages with different build systems. It's convenient to have the whole thing open (search in the whole repo, check that a change in one project doesn't break the other ones, ...)

This is not a total deal breaker. It would just make the experience better. I've created #873 to keep track of the idea.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. Well I'm definitely not against the idea.

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.

None yet

6 participants