Closed matklad closed 6 years ago
In intelliJ, cancellation is an important feature of the platform. It works like this:
All data is protected by a single RwLock. Only main thread can .write()
it, any thread can .read()
it. When the user types, main thread grabs .write
, applies changes and starts async annotation tasks on the threadpool. Each annotation task grabs the .read
lock. If the user types when annotations are in progress, the main thread cancels all the tasks before grabbing the lock. This cancellation ensures that changes are incorporated quickly.
Sync actions operate on the main thread directly, under the write lock.
When thinking about this in the context of salsa, three solutions come to mind:
serialize "modification" and "querying" phases, so they never are initiated concurrently. This is how rustc
works, and what is implemented right now.
provide "value semantics" for each generation. That is, allow different versions of the world state to exist simultaneously, and allow querying both up-to-date and stale data. Conceptually, this is appealing, but it seems hard to implement (we'll need to remember more than one version of each query) and not really useful (you don't really need stale results)
provide "auto cancellation". That is, if the generation is advanced while a query is in progress, it gets cancelled as soon as it tries to do anything with the store. This doesn't seem too hard to implement, and gives us cancellation "for free", but it adds another error condition to every query.
See https://github.com/salsa-rs/salsa/pull/48 which offers some potential building blocks. I admit I didn't realize there was so much text on this issue! I'll read and compare.
Continuing https://github.com/nikomatsakis/salsa/pull/4#issuecomment-425715114 discussion.
This is definitely a good start, but, for IDEs, I think we'll need something better eventually. Let me present a couple of important IDE interaction patterns.
Async Annotations
These are the cases when IDE reacts on user's input or external changes. Examples: syntax highlighting, warnings and errors (which all are various text decorations). It's OK if they appear with a noticeable delay after the user types. However, for syntax highlighting and syntax errors specifically, a noticeable delay is noticeably annoying. For this reason, in IntelliJ they are computed in phases: first, lexical analysis is used to apply basic highlighting, then, parsing is done to report syntax errors, then, precise syntax highlighting is done (which uses some semantic information), and, lastly, errors and warnings are computed.
For this case, it's important to be able to apply changes and compute syntax highlighting without waiting for previous semantic analysis results (which might take some time to calculate, especially if we have a mode that computes errors across the whole project, and not only opened files). Failure to do so will be annoying (why I sometimes get syntax errors immediately, and sometimes after a delay?), but most certainly not a deal breaker.
Sync Actions
These are the cases when IDE reacts to the explicit user's request. Examples: indenting cursor after Enter, re-indenting lines on
}
typed, extend selection. Most of these things need only a syntax tree, but sometimes semantic is useful as well, for example, in postfix-templates case. Postfix template is this thing: user has an expression, whose value is an enum:compute_thing(stuff)
, user types.match
and pressed tab:compute_thing(stuff).match
, and gets a match statement with filled variants:match compute_thing(stuff) { Ok(think) => ... }
.For such "sync" actions, latency directly affects the user, so being able to apply changes while a long-running computation on the old state is running, is not simply a QoL improvement, but rather a question of "can we do this feature at all?".