haskell/haskell-ide-engine

Decide on matching an existing IDE protocol or designing a new one

Closed this issue · 27 comments

We can either use / extend an existing protocol, or have an entirely new one. Lets discuss what will work best.

Pick an existing protocol

If we pick an existing protocol, then ideally, this would be one that has many editor integrations. So, the primary benefit of doing this would be to get lots of code reuse and editor support "for free". Unfortunately, it doesn't seem like there is yet a de-facto standard here. Even if there was, I'm thinking that picking something which is intended for a non-haskell-ey language will be too much impedance mismatch.

There may well be some potential for cross-polination with haskell-like languages, though. Idris's protocol seems like an interesting possibility (see #3).

New protocol

Do a new protocol. To do this I think it'd make sense to take a look at all the existing protocols, and consider the attributes of its design. Here are a few GHC specific protocols to take a look at:

  1. stack-ide, which also use the types from ide-backend-common. The particularly nice thing here is the amount of juicy details yielded by ide-backend-common, particularly in IdInfo. However, it also ends up being up to the editor what to do with all this this info.

Something I think should be done differently than this protocol is to provide less mutation. Instead, the query protocol should just give ways to ask for info about code, based on what's in the filesystem and the CLI arguments. Being able to change GHC flags on the fly is pretty cool, but getting it perfectly correct is tricky.

  1. ghc-server has some rather nice protocol types, but for Chris has been superseded by

  2. ghci-ng, which simply adds a few new ghci commands which yield machine-readable output. This is also the approach taken by Idris's repl based IDE integration.

I've got some more thoughts on what such a protocol ought to look like / provide, but I'll save it for later :)

alanz commented

If we ignore the fact that it uses zeromq for the transport layer, the IPython backend may be something to look at too, especially in the light of the IHaskell project.

See http://andrew.gibiansky.com/blog/ipython/ipython-kernels/ for an overview of the architecture and http://ipython.org/ipython-doc/dev/development/messaging.html for an overview of the messaging

alanz commented

ping @david-christiansen : Do you have anything to add here

Hi there! I'm in the middle of the final push on my PhD dissertation. So I'll have to get back to you later WRT lessons learned from Idris's IDE backend. In the meantime, it might be worth hearing what @archaeron and @Melvar think, as they've implemented clients of the protocol and are probably in a position to comment on how well it works.

Idris's protocol is based on the one used in SLIME, DIME, and Ensime, so those are definitely worth looking into as well. The Lispers had nice programming environments nailed a long time ago :-)

I would advice splitting human oriented free text interface from application oriented interface. There were and are issues in haskell-process that result in lockups and hard to debug issues if those two modes of operation are mixed.

I second @gracjan's suggestion. A (fairly) common problem for me with haskell-mode is that I'll start a long-running task in the inferior haskell process (e.g. a web server), and then emacs promptly hangs because company-mode tried to talk to the same process to get completion candidates.

Hello
I'm working on the atom package for idris. I can tell you what I like about the protocol.

Format

The format is lisp-like and therefore really easy to parse. The parser I implemented is about 40 lines long: https://github.com/idris-hackers/atom-language-idris/blob/master/lib/parse.coffee.
it doesn't really matter what language you use, the parser will be pretty simple and that's a big plus.
On the other hand it's a bit harder to parse for a human, because things don't have any labels (JSON would have those for instance).

Protocol

The only problem with the protocol arised when it didn't give me enough information about where to put code, or where an error happened, ...
See:
idris-hackers/atom-language-idris#72

I think the protocol should also handle things like using the right modules in your code, a bit like stack ghci or cabal repl read your cabal file to use the right imports.

Extending the REPL with a few commands and making it machine readable is a really good idea anyway. People are familiar with the existing repl and you get many things for free.

This is what I can come up with at the moment. Not very technical, I know, but I hope it helps. I'm open for questions if you'd like to hear more.

TL;DR: I really like the approach idris is taking and enjoy using it for the atom package.

I second @gracjan's suggestion. A (fairly) common problem for me with haskell-mode is that I'll start a long-running task in the inferior haskell process (e.g. a web server), and then emacs promptly hangs because company-mode tried to talk to the same process to get completion candidates.

Yup, that's definitely on the agenda. In ide-backend on posix, this was done by using forkProcess, for the following reasons:

  • From a comment by @edsko:

    We are using forkProcess rather than forkIO. This is necessary because running a snippet requires changes to the global process state: in particular, we need to redirect stdin, stdout and stderr (and, Haskell side, we use withArgs to change the value returned by getArgs). While this doesn't affect the API as such, it is worth bearing in mind that these forks are not as cheap as forkIO. Each new process communicates with the client through a new triplet of named pipes, which are created in the session temp directory.

    In other words, in order to fully control the process's execution environment and isolate interaction with stdio, it needs to be in a new process. forkProcess is a nice way to do this efficiently, but it doesn't work on windows. I suppose the windows alternative would need to be reloading the interpreter from scratch.

  • Another reason, from my own comment in the same thread:

    Beyond needing to change the global process state, another reason to use forkProcess is that you can't reliably kill all the threads that a given program starts. For example, with our own ghci-runners for development (yesod devel, or my own turbo-devel), a tricky issue is that cancelling a thread in ghci (via Ctrl-C) doesn't kill the threads that it's spawned. So, the server will continue running, and hold the resources that are needed by subsequent sessions. The workaround is to use a fresh ghci for every reboot.

  • This also allows for isolation of the main info-providing process from crashes caused by the executing code.

For running the main function of a server, this is a great thing to have. However, I don't think it should be used for all executions in the REPL.

@archaeron Thanks for the info on the protocol!

The only problem with the protocol arised when it didn't give me enough information about where to put code, or where an error happened, ...

That's an interesting issue indeed! I'm curious about the cases where it's unclear where an error happened. Is this something that comes from the protocol itself? A couple thoughts on making the protocol better for error cases:

  • Have a uniform way of sending back an error response when something fails, explaining the failure.
  • Always include a tag which indicates the type of response. This way there are more descriptive exceptions when the response isn't the variety that the client expects. Perhaps this isn't so important, though, and we should just make sure that the clients and server agree upon.

The Idris protocol does tag every response as either an error, a bit of intermediate output, or a final answer. It's written up here. I think that @archaeron is referring to the fact that our tracking of source locations is still not good enough, and some responses still put too much burden on the editor to choose where to put things.

For instance, if I ask Idris for an initial pattern match clause for:

add : Nat -> Nat -> Nat

it will reply with "add j k = ?add_rhs", and the editor has to figure out where that goes. If the type signature is on multiple lines with line breaks and such, then the editor has to work hard. It would be better if Idris said "put it on line 10", because Idris has a parse tree and doesn't need to use heuristics. This is on my todo list.

Also, some errors' locations are still reported somewhat vaguely. For example, Idris will say "the problem is somewhere on the RHS of this definition" rather than "the problem is in the expresssion that starts on line 132 column 10 and goes to line 133 column 15". Again, this is one of those things that will just take hours of work to fix, but that we'll eventually get to.

Finally, I agree with @gracjan that the protocol must be run out-of-band. In Idris, we send the IDE protocol over a socket so that editors can wrap stdio however they want. That way, if the compiler does need to do IO (e.g. if there's a type provider that uses stdin/stdout), the editor can display it directly. In Emacs, this is done by running Idris itself in comint mode and then opening a socket to the same process for editor commands.

So far we haven't needed Idris to be concurrent to support the editor, but it might be the case eventually. Right now, if I ask for docs on a symbol while the compiler is busy type checking, then it just waits until type checking is done to give me the docs.

BTW, I agree about using JSON as the format. As much as I dislike it, it seems to have won, and not having to write a parser is better than writing a trivial parser. The sexprs in the Idris IDE protocol reflect its background in SLIME.

alanz commented

@david-christiansen thanks for taking the time to respond so constructively to this thread

@alanz No prob! Ping me again in a few weeks if I haven't arrived with any high-level reflections on how it works.

I can’t really say much about the qualities of Idris’ ide-mode, since I only use essentially one command and throw out the majority of the info in the response, which is enough to simulate an Idris REPL on IRC including color.

The protocol I ended up designing for psc-ide uses a query format similar to the one of Elasticsearch, where you build up a query by composing JSON objects.

This way one can keep the protocol extendable.

It might also be nice to offer both a synchronous and an asynchronous protocol to relieve the editor plugins of the need to handle processes on windows.

alanz commented

I think it would be good to define the protocol at a logical level, and then provide different transport layers to cater for the details of the various client IDEs

epost commented

@alanz +:100:. That sounds like an immensely useful thing to do, also wrt cross-language support.

epost commented

Here's a rough sketch of some of my thoughts regarding a protocol that separates commands and queries (and would be reusable across languages): https://gist.github.com/epost/de87e67558a18de6716a. The model specification is in terms of datalog.

By the way, I think that whatever protocol you end up putting together, it should enable this interaction:

emacs1

emacs2

emacs3

emacs4

Although for Haskell, "Expand type aliases" or "Evaluate type families" might be a better name for the command than "Normalize term".

I think as a first step for this project trying to unify all of those is just too hard to accomplish let alone "get right". Instead we should be trying to design the architecture of haskell-ide such that we can, for the time being, accommodate old protocols and keep working towards a more unified approach to replace the other ones by bit by bit.

Committing to an interface/protocol at this stage would just not make sense to me.

alanz commented

I think the sooner we can get something concrete on the ground, that does small useful thing and connects to an IDE the better.

But equally, we must be careful that we don't put down a "provisional" comms mechanism which is then entrenched forever.

Balance.

Seconding JSON.

In case a pre-existing protocol isn't selected, here are some notes from what I said in IRC:

21:17 < bitemyapp> why not return a promise id and let the client decide when to block on getting the response?
21:17 < bitemyapp> GET /type/?data=blah -> returns {"promise": 0}, then you GET /promise/0 when you're ready for the answer.
21:19 <@alanz> bitemyapp, how does it work if there are other async events coming too, and/or it is a long running command so there is no knowledge in the front end of when it has finished?
21:19 < bitemyapp> alanz: get -> 1, get -> 2, get -> 3
21:19 < bitemyapp> alanz: okay, call them continuation or callback ids. point is to uniquely (but temporarily) identify an ongoing computation across the wall.
21:20 < bitemyapp> alanz: this is a bit like codata.
21:20 < bitemyapp> alanz: you can either block or give them a chance to block on another continuation.
21:20 < bitemyapp> alanz: you could also let them assert a timeout.
21:20 < bitemyapp> alanz: "timeout=5ms", then if you don't have an answer within 5 ms, give them a new k id.
21:21 < bitemyapp> but seriously, this gets into the problem that codata solves in total languages.
21:21 < gracjan> bitemyapp: good idea with those timeouts
21:21 < bitemyapp> in those languages, if you have an indefinite stream of data, you have to keep giving control back to the caller before you can keep working.
21:24 < bitemyapp> JSON has wide and popular support in a variety of languages
21:25 < bitemyapp> supporting a uniform JSON representation will be a lot easier here.
21:26 < bitemyapp> gracjan: why wouldn't you want a threaded server backend for the API?

I don't much care for stateful channels/sockets for this, but I could be argued into them if it could be shown that the overhead of an HTTP request to localhost is human-noticeable. Otherwise, I don't think it's a good idea.

@gracjan suggested letting clients generate the id, this is a good idea I think.

saep commented

I've written a plugin manager for neovim, which talks to neovim using the msgpack-rpc specification. It's a simple specification that is straightforward to implement and flexible. You can send and retrieve almost arbitrary (JSON-like) objects, so the expressiveness should be sufficient. Since neovim uses it, the number of languages which (need to) support the spec is also large enough. I don't think that the msgpack format format's "smallness" is very important, so it might be a better idea to use JSON as the representation of the data because of the far broader library support.

@saep I'll be giving this a serious look, thank you.

Can this be closed? The protocol seems to have become at least somewhat stable so if we plan to make major changes, we should either do that as soon as possible or stick somewhat to what we have now.

alanz commented

In terms of triggering a concrete action right now, this can be closed. Not sure if any of the discussion should be moved to docs though.