[HN Gopher] Zig language server and cancellation
       ___________________________________________________________________
        
       Zig language server and cancellation
        
       Author : goranmoomin
       Score  : 190 points
       Date   : 2023-06-10 06:40 UTC (16 hours ago)
        
 (HTM) web link (matklad.github.io)
 (TXT) w3m dump (matklad.github.io)
        
       | tlarkworthy wrote:
       | I would probably go for immutability, which unlocks memoization,
       | which unlocks reuse of parsing computation. Then do everything
       | strictly (no cancellation) so the whole this is super fast for
       | the common case. I do wonder if grabase collection is too hard to
       | solve though for memory managed languages. Reference counting?
        
       | Szpadel wrote:
       | > We can think about applying something like that for
       | cancellation -- without going for full immutability, we can let
       | cancelled analysis to work with the old half-state, while we
       | switch to the new one
       | 
       | if we want to cancel analysis basin on old state why do we need
       | garbage collection and two states?
       | 
       | And how is garage collection way different from full
       | immutability? In my understanding this is the same thing but with
       | extra steps, you have to copy element instead updating it in
       | place because existing copy might be used by already running
       | analysis. Reference counting should give you similar benefits
       | with less work needed, usually everything have only single
       | reference (no analysis is using anything) and we could optimize
       | it to modify it in place in that case, otherwise you just need to
       | copy it once without requiring garbage collection later.
       | 
       | Am I missing something?
        
       | samsquire wrote:
       | I am deeply interested in the multithreading, parallelism, async
       | and coroutine design space and I journal about it everyday in my
       | ideas journal. I am also interested in cancellation.
       | 
       | I wrote a toy very simple 1 scheduler thread:M kernel threads:N
       | lightweight thread runtime in terrible Rust, C and Java.
       | 
       | Hot loops use a structure for its limit and looping variable.
       | Then to cancel the loop, you set the looping variable to the
       | limit from a scheduling thread, cancelling the loop. This is used
       | for process switching and scheduling but it can also be used for
       | cancellation.
       | 
       | Can create very responsive code this way, it's even possible to
       | cancel while (true) loops by replacing them with while
       | (!preempted) {}.
       | 
       | https://github.com/samsquire/preemptible-thread
       | 
       | There is potential for a race, but that can be detected and
       | worked around.
        
         | girvo wrote:
         | Silly question, because I've been dealing with exactly this
         | complexity in FreeRTOS and haven't been able to solve it, but
         | if you're doing while (!preempted) doesn't that mean that the
         | preemption can only stop the loop after the current iteration
         | of it finishes? I'll dig into your code, but I've not been able
         | to think of a (user space) way of cancelling a blocked thread
         | (such that the thread is properly cleaned up/scope closes and
         | destructors are called) myself
        
           | samsquire wrote:
           | Yes that's right.
           | 
           | I think as the other commentator said that you would need to
           | use signals to interrupt within between elements of a loop.
           | 
           | I try not interrupt a hot for loop or while true loop to
           | reduce performance impact of an if statement in a loop.
           | register_loop(loops, 0, 1000000);       for (;
           | loops[0].loop_variable < loops[0].limit ;
           | loops[0].loop_variable++)        {         // Hot loop here
           | }
           | 
           | It's difficult from another thread to know where in the loop
           | another thread is in.
           | 
           | Then from another thread
           | loops[0].loop_variable = loops[0].limit
        
           | brabel wrote:
           | As far as I know, it' basically impossible to interrupt a
           | blocking IO call unless you're using something designed for
           | preemptive scheduling, like epoll[1].
           | 
           | A Java Thread can be interrupted, but only if the runnable
           | implementation "cooperates"... if it's blocking on a
           | synchronous socket "read", it's going to stay blocked even
           | after the Thread has been interrupted.
           | 
           | In a language like Dart, where async is mostly the default,
           | it's possible to cancel every job running on a "Zone"[2]
           | because the mechanism used for creating a new Future is
           | interceptable by application code, which is pretty cool.
           | 
           | Kotlin coroutines have a mechanism for cancellation built-
           | in[3] and it's also "cooperative", i.e. all suspend functions
           | will cancel within the coroutine context, but a rogue
           | coroutine that blocks on a blocking IO call will remain
           | blocked (as I understand it - not sure if Kotlin provides
           | ways to completely avoid blocking IO).
           | 
           | So I guess whatever language you use, there has to be points
           | of "yielding" control where cancellation can occur... the
           | `while (!preempted)` check is just one of them.
           | 
           | [1] https://man7.org/linux/man-pages/man7/epoll.7.html
           | 
           | [2] https://dart.dev/articles/archive/zones
           | 
           | [3] https://kotlinlang.org/docs/cancellation-and-
           | timeouts.html#c...`
        
             | Matthias247 wrote:
             | You can interrupt blocking IO calls using signals, so there
             | is an alternative to using epoll/select for this. It can
             | also be used to implement timeouts if another userspace
             | thread tracks the IO operation duration and sends a signal
             | to the IO blocked thread if the max duration has expired.
             | 
             | I think Java thread interruption for blocking IO might be
             | based on signals and is not using nonblocking IO under the
             | hood. But I would need to read JDK source again to tell for
             | sure
        
               | girvo wrote:
               | Yeah signals is what I've seen too, though I'm unsure if
               | newlib + FreeRTOS has enough to make 'em work! I'll have
               | a look though.
        
       | KRAKRISMOTT wrote:
       | Can this be done on the runtime level? Zig, if I recall, supports
       | multi-colored functions. Would it be possible to terminate an
       | awaitable early?
        
         | valenterry wrote:
         | Multi-colored? Google doesn't spit out anything meaningful and
         | ChatGPT claims that Zig didn't exist in Sept 2021...
        
           | a1369209993 wrote:
           | They're (presumably) refering to "colorblind" functions.
           | (Note that "blind" is apt here: the color isn't _visible_ ,
           | but functions still have color and will break in subtle ways
           | if you try to use them as if they don't.[0])
           | 
           | Zig's approach is probably a improvement over bare
           | async/await, but pre-javascript languages already had _more_
           | of a improvement in the form of _not having_ async /await.
           | Implementing async/await _at all_ makes the language _worse_
           | unless there 's a (preferably provably) universally-
           | applicable way for functions to be (explicitly) generic about
           | whether they're async or not.
           | 
           | 0: https://gavinhoward.com/2022/04/i-believe-zig-has-
           | function-c...
        
           | 59nadir wrote:
           | I think in popular parlance the terminology comes from the
           | "What color is your function?" article[0]. The idea is that
           | "normal" and async functions are different colors and they
           | don't mix. When you call an async function from a normal one
           | you make the normal one async, i.e. if it had no color it now
           | takes on the color of the async one, etc.
           | 
           | Zig has a system where calling an async function is actually
           | doable while still leaving the calling function non-async.
           | 
           | 0 - https://journal.stuffwithstuff.com/2015/02/01/what-color-
           | is-...
        
             | valenterry wrote:
             | Oh yeah, I know about that. But that is commonly called
             | "colored functions" not "multi colored functions" no? So I
             | thought there was a difference.
        
               | littlestymaar wrote:
               | Zig aims to solve this "problem" by having functions that
               | can be either async or not.
        
               | wtetzner wrote:
               | Right, colored functions are e.g. async or not async. In
               | Zig, you can write a single function that works as both,
               | which I assume is what multicolored means here.
        
               | valenterry wrote:
               | That doesn't seem to make any sense to me - it should be
               | then called uncolored functions no? Because they all look
               | the same.
               | 
               | But maybe that's what they ment to say.
        
               | kps wrote:
               | They're generic over 'color' at compile time; the
               | compiler generates code for normal and/or async contexts
               | as necessary.
        
               | valenterry wrote:
               | Right - so to the developer they all look the same, which
               | means they are uncolored. If they are colored it means
               | they look different (e.g. async vs sync).
        
       | ninepoints wrote:
       | As someone actually working on some symbol indexing stuff at the
       | moment, I can mention that this model is ignoring some pretty
       | important details, and I think there's some classic
       | overengineering happening here (easy trap to fall into when
       | you're still in the design space).
       | 
       | Generally, when you need a code completion action, it's expected
       | that you have an AST (post semantic analysis). This AST is still
       | very useful even after a user continues to edit the code! Outside
       | of where the cursor is, most of the symbols, definitions, and
       | declarations are relevant, and source locations can be easily
       | translated from a previous version of the document, provided that
       | deltas are tracked. Cancelling an existing translation unit parse
       | on edit is wasteful, because the majority of the time, that parse
       | will produce meaningful results. The better approach (IMO) is to
       | let the parse finish, but immediately enqueue a subsequent parse
       | (with some debouncing timer to avoid overly consuming user
       | resources). If you wanted to level this up further, you could
       | perform incremental parsing/analysis, provided your language
       | supports it. In the presence of a preprocessor, this can be very
       | difficult, but it's the next "upgrade" from the previous approach
       | mentioned in my opinion.
        
         | matklad wrote:
         | This depends on the compilation model in use. If that's a
         | traditional pipeline of phases, where the result is an AST data
         | structure for the whole CU which gets annotated with types,
         | then, yes, "enqueue new analysis" makes sense.
         | 
         | If the compilation model lazy & query based, then theres's just
         | no "enqueue a subsequent parse", but rather "give me the type
         | on this thing doing as little analysis as possible". You got to
         | re-use symbols and declarations not because you heuristicsly
         | adjust offsets and assume they are otherwise valid, but because
         | underlying analysis reasons, precisely, they they are valid and
         | re-usable.
         | 
         | The two models are quite different at the core, and aren't
         | really an evolution of one into another.
         | 
         | Which of the two approaches to use, and, consequently, whether
         | to think about cancellation at all, depends heavily on the
         | language in question. If the language allows for lazy analysis,
         | it's probably a better bet, as that gives you correct results
         | faster.
        
           | ninepoints wrote:
           | I only know a little bit about zig, but I would have assumed
           | that the latter mechanism you are describing isn't really
           | possible. Most languages with "advanced" features and type
           | systems really need a full semantic tree to do anything
           | meaningful due to the complexities of compile time code and
           | type instantiation.
           | 
           | Even if we are using this latter model, the user is only
           | editing one file at a time. What if the completion source is
           | in a different file altogether? Generally, you're going to be
           | indexing the entire codebase anyways, so we're splitting
           | hairs over a potential optimization in just one facet of the
           | indexer.
        
             | matklad wrote:
             | > Most languages with "advanced" features and type systems
             | really need a full semantic tree to do anything meaningful
             | due to the complexities of compile time code and type
             | instantiation.
             | 
             | I wouldn't say this depends on abstract complexity of "type
             | system". For example, Rust is pretty advanced, but lazy
             | query architecture works well for it. Usually, it's the
             | name resolution/macro expansion that puts the wrench in the
             | works, not a Turing-complete type system.
             | 
             | That being said, yes, in Zig you probably can't do lazy
             | query-based IDE. It really is a compile-time smalltalk, and
             | wants to have an image.
        
               | ninepoints wrote:
               | I guess I meant complexity in the sense of symbol
               | resolution in this context. So things like scoped type
               | aliases, expressions evaluated as template or trait
               | arguments, that sort of thing.
        
             | vlovich123 wrote:
             | I doubt you need a full semantic tree. Just the parts that
             | are accessible to the local edit scope. Like if I break the
             | code within a function, all other code in the unit should
             | be ok. It gets trickier with broken syntax but there's a
             | lot of clever recovery techniques I've seen.
        
               | ninepoints wrote:
               | How do you propose code completions are done without it?
               | To even produce a set of suggestions, you need to
               | understand the parse context, the set of available
               | symbols with appropriate types and scope resolution. AST
               | parsers already have recovery mechanisms, and I am
               | assuming those are all working as intended.
        
               | vlovich123 wrote:
               | When I'm in function foo I may have a local closure X.
               | When I'm editing bar I don't need to consider X, right?
               | 
               | I think what you're trying to say is "how do you pick a
               | subset of a tree to load at any given point" / "it's
               | simpler to have the entire semantic tree and traverse it
               | than trying to keep a live tree". If that's a correct
               | reading, I agree, it's a difficult problem. I don't have
               | any specific recommendations other than to note it's a
               | graph and graph databases don't need to keep the entire
               | graph in memory to do queries so there must be something
               | similar you could do with code. The other part is that
               | not all parts of the graph are equally relevant so
               | intelligently pruning it should result in better
               | completions. I explored this mildly as an undergrad but
               | the topic never sufficiently interested me to continue
               | pursuing it.
        
             | 59nadir wrote:
             | > Even if we are using this latter model, the user is only
             | editing one file at a time.
             | 
             | I'm not necessarily arguing against your bigger point but
             | this is very frequently a wrong assumption and strikes me
             | as designing around an idealized view of the problem that
             | you would _like_ to be the case, not actual reality. Code
             | generation scripts, formatters, auto-fixers and the like
             | can modify many files and many parts of those files  "at a
             | time", unless you have a pedantic view of what "at a time"
             | means. Almost all LSPs fail in these modes of operation and
             | disallow external tools from participating in the code
             | base, which is very annoying and makes them less useful.
             | 
             | Having to actually (re-)open a file so the LSP can "see"
             | changes made to it by an external program is not something
             | that should ever be needed but happens a lot with some of
             | the most worked-on language servers (TypeScript comes to
             | mind).
        
               | lozenge wrote:
               | According to the spec it's up to your IDE to notify the
               | LSP when files change (even due to an external software)
               | then the LSP reads the new versions.
               | 
               | Typescript also doesn't use an LSP as it predates the
               | introduction of LSPs.
        
               | saghm wrote:
               | What does it mean for a language "not to use an LSP"? The
               | most straightforward way I can think to interpret that
               | statement is that it's saying that a TypeScript language
               | server doesn't exist at all, which certainly doesn't seem
               | to be the case based on finding
               | https://github.com/typescript-language-server as the top
               | result when googling "typescript language server". I
               | guess you might be saying that it's not a first-party
               | language server, but I'm not really sure why that would
               | be relevant when discussing properties of language server
               | implementations; the implementation clearly exists, and
               | even if very few people are using it, that doesn't mean
               | GP's point about a flaw in its design wouldn't still
               | apply.
        
               | ninepoints wrote:
               | Either way, all those files need to be marked dirty and
               | reindexed, so I suppose I'm just not sure how the topic
               | at hand is relevant. Is the proposal that we attempt to
               | treat each dirtied file as though incremental edits were
               | imminent? Because this is precisely the wrong assumption
               | you're raising. Ultimately, if you need to reopen a file
               | in order to see changes on disk reflected, that's a bug
               | with your lsp server, nothing more.
        
               | 59nadir wrote:
               | > Either way, all those files need to be marked dirty and
               | reindexed, so I suppose I'm just not sure how the topic
               | at hand is relevant.
               | 
               | You stated an assumption as part of your argument (only
               | one file is being edited at a time) and that assumption
               | is very often false. If that assumption doesn't matter I
               | don't know why you brought it up in the first place. I
               | stated pretty clearly that I wasn't arguing against your
               | larger point but the assumption you explicitly stated was
               | false.
               | 
               | > Ultimately, if you need to reopen a file in order to
               | see changes on disk reflected, that's a bug with your lsp
               | server, nothing more.
               | 
               | You don't say? It's the kind of misbehavior you get when
               | people make assumptions that are false and bake them into
               | the design of things.
        
               | ninepoints wrote:
               | And that assumption is relevant in the context of OPs
               | concern but not yours. You created a different
               | hypothetical and orthogonal situation where the
               | assumption need not apply, and to be honest, I lost the
               | plot a bit. It's possible for there to be a branching set
               | of concerns, each of which having their own set of
               | perfectly valid but otherwise disjoint assumptions.
        
             | trashburger wrote:
             | This isn't the case for Zig, because the compiler works off
             | a graph of declarations. You only need to compute the
             | declarations referenced in a code block (which have well-
             | defined sources, since you cannot create new decls at
             | comptime right now) in order to compute the function
             | itself.
             | 
             | >What if the completion source is in a different file
             | altogether?
             | 
             | Well, yes, this is a very common case. I don't believe
             | matklad is arguing that the computation should only be
             | restricted to one file. From what I can gather from the
             | article, these "working" and "ready" copies would be per-
             | file as well and the "ready" states of each file would be
             | invalidated as a referenced file gets edited.
        
         | levodelellis wrote:
         | I pretty much entirely agree with you. Below is a copy/paste of
         | what I said when I saw this article elsewhere. What language is
         | your LSP for?
         | 
         | > I written a LSP for my prototype compiler. I don't like any
         | of the options you listed. My LSP didn't do any typechecking,
         | it didn't build an AST, it didn't need immutable data
         | structures etc.
         | 
         | > Typically when a person is typing into the editor the code is
         | in a broken state (incomplete variable name, missing semi
         | colon, maybe an open but no close parenthesis etc). What I did
         | was look around what part is being edited and using the
         | previous 'build' (when a user saves or ask the compiler to
         | build), I would look up vars and type names. There's no need to
         | rebuild everything on every keystroke. Maybe you can do it on a
         | newline if you really wanted to but midsentence sounds like a
         | bad place to try and you're not really gaining anything from
         | compiling/parsing a single line change
        
       | riwsky wrote:
       | > We divide the available memory in two equal parts, use one half
       | as a working copy which accumulates useful objects and garbage,
       | and then at some point switch the halves, copying the live
       | objects (but not the garbage) over
       | 
       | So, generational garbage collection?
        
         | tankenmate wrote:
         | Think of it more like a blue / green object store; but each
         | switch only copies over valid objects (for some definition of
         | valid; in use, type checked, etc).
        
         | spullara wrote:
         | Nah, just a copying gc:
         | 
         | http://www.cs.cornell.edu/courses/cs312/2003fa/lectures/sec2...
         | 
         | Generational has more heaps where objects are promoted between
         | them when they last longer.
        
           | dontlaugh wrote:
           | More specifically, semi space.
        
         | esjeon wrote:
         | There's no generation here. Just live or dead objects. It also
         | sounds like they don't scan the pool, and instead just deep-
         | copy the top-level state object.
        
       | avgcorrection wrote:
       | Should a language be designed with these things in mind? Or is it
       | completely orthogonal?
        
         | matklad wrote:
         | Descriptively, yes, the shape of the language determines how
         | you can structure an interactive compiler. There are at least
         | three wildly different ways to do that:
         | 
         | https://rust-analyzer.github.io/blog/2020/07/20/three-archit...
         | 
         | Which one can work depends on the language in question. Java,
         | Rust, C++ lead to different answers, and that's not because of
         | the "interesting" differences like borrowchecker vs gc, but
         | rather due to the "boring" differences in name resolution and
         | module system.
         | 
         | Prescriptively, whether we _should_ do this (or, rather, how
         | much), is unclear.
         | 
         | My biased answer is that, while language designers talk a lot
         | about making languages tooling friendly, there's little of that
         | actually happening (at least with "current" languages, "next"
         | ones seem to fare better). Like, it was said that "rust macros
         | are designed with tooling friendliness in mind", but overall
         | the language is pretty tooling-hostile, mostly for accidental
         | reasons. It seems to me that if we _actually_ co-design a
         | tooling-first language, without trying to innovate too much,
         | but by just ensuring that existing techniques work robustly, we
         | might arrive at a close, but meaningfully different point in
         | the design space.
         | 
         | Specifically, here's my version of IDE-friendliness diff for
         | language design:
         | 
         | Push conditional compilation far further in the pipeline, such
         | that it is done after all semantic analysis. This is a pre-
         | requisite for making automated refactors which are _guaranteed_
         | to work.
         | 
         | Similarly, push meta programming further down, such that code
         | analysis doesn't invoke user-defined code (which might be
         | arbitrary slow), but, at the same time, meta parts can fully
         | reflect on existing code, including resolved types. C#-style
         | source generators are an interesting design here.
         | 
         | Have strong signatures on micro and macro level. Macro, have
         | well-defined compilation units with explicitly specified dag of
         | dependencies and signature files. Micro, annotate types of the
         | functions. Use tooling to reduce double-annotation burden.
         | 
         | Ensure that each source file can be somewhat deeply analyzed in
         | complete isolation. The last two points should unlock both
         | embarrassingly parallel (distributed) compilation, and snappy
         | completion.
        
           | littlestymaar wrote:
           | > Push conditional compilation far further in the pipeline,
           | such that it is done after all semantic analysis
           | 
           | So much this. I've idea how this would work un practice (like
           | how you're supposed to accomodate antagonistic semantics
           | which depend on the conditionnal compilation), but I hate
           | _#[feature(X)]_ with passion because of how brittle they are.
        
           | ComputerGuru wrote:
           | Alex, I've been reading all your posts on Zig and Rust LSPs
           | with great fervor and appreciate everything you've done for
           | the community with rust-analyzer. Can I ask you about clangd,
           | the llvm/C++ lsp? It seems to work tremendously well, far
           | better than I naively ever thought it would even on massive
           | codebases. I was using it from its first release and the
           | quality ramped up rather quickly. Can I bug you to write up
           | about it from your perspective and with your insights
           | sometime and where we could benefit from their design choices
           | and where we can't? And of course, the limitations of their
           | approach?
           | 
           | It's ostensibly driven by the `compile_commands.json`
           | produced by the compiler during a run (mapping each source
           | file to the output .o and the command (w/ all options) that
           | was used to generate it) but in reality that's just because
           | C++ doesn't have its own build system so the tooling has no
           | other way of knowing how a project is wired up. But aside
           | from that, it's extremely fast and has fairly low latency for
           | a language that's got a lot of the same warts that rust has
           | (generics, monomorphization, macros, strong types, (limited)
           | type inference, etc) especially when compared to all the
           | other LSP offerings (although I think C++ compilation units
           | are smaller than rust's, which has to help).
        
             | matklad wrote:
             | I covered this a bit here:
             | 
             | https://rust-analyzer.github.io/blog/2020/07/20/three-
             | archit...
             | 
             | I don't actually _know_ how exactly clangd works,
             | especially post modules, but pre-modules C++ has a
             | compilation model which is actually quite friendly for an
             | IDE, because of the header files.
             | 
             | Headers essentially explicitly encode body/interface
             | separation that more advanced incremental compilation
             | systems like Salsa try to recover implicitly.
             | 
             | Looking at a slightly simplified typical C++
             | #include <iostream>              void main() {
             | std::cout << "Hello, World!" << std::         }
             | 
             | what a language server could do is that it can run bog-
             | standard phased compiler, and just _freeze_ its state after
             | all includes are parsed&expanded. After that, any typing
             | inside the file needs to re-analyzer just the file itself.
             | Also, the code in included `.h` files is typically far
             | smaller then the code in the corresponding `.cpp` files.
             | So, a language server gets ability to analyse only the
             | relevant code, and skip the rest, for free, it is encoded
             | in compilation model.
             | 
             | Compare this with Rust, where, in the general case,
             | changing a file can affect any other file in CU in
             | arbitrary ways (_usually_ it doesn't, but it _could_, and
             | the complexity lies precisely in figuring out which edits
             | are local and which are not). And CU's themselves are
             | meatier, as macro expansion can run arbitrary code, so you
             | need to somehow cache that, taking into account that macro
             | expansion _anywhere_ in CU can potentially affect
             | _anything_ in CU. So, you need full build systems a-la
             | carte incremental compilation, which is a) hard b)
             | different from how the command line compiler works.
             | 
             | The second point explains the success of clangd I think:
             | _usually_, when you have a command-line compiler and want a
             | language server, you want to rewrite, because the
             | architectures are too different. But because of the way C++
             | (and OCaml) work, for those two languages you actually can
             | soundly re-use existing code with relatively minor
             | modifications.
        
           | avgcorrection wrote:
           | Thanks for that.
           | 
           | It seems that some newer languages (like in the last ten
           | years) either care about (1) batch compilation speed or (2)
           | language design mostly unconstrained from having to have fast
           | batch compilation. It would be interesting to see a third
           | way: language design that takes into account all kinds of
           | future interactive tooling by laying a good groundwork. So
           | maybe from-scratch batch compilation ends up slow, but then
           | it turns out to be fine in practice since you can distribute
           | builds and cache things so that you only pay at most a one-
           | time large cost. And then what you mostly have to deal with
           | is incremental stuff.
        
             | firstlink wrote:
             | Interactive proof languages already work that way, to a
             | greater or lesser extent.
             | 
             | But, for general-purpose languages, this revictimizes the
             | docker addicts.
        
           | the_mitsuhiko wrote:
           | > there's little of that actually happening (at least with
           | "current" languages, "next" ones seem to fare better).
           | 
           | I'm not too involved but I think the languages that actually
           | develop the tooling as part of the language design have
           | traditionally done quite well at it in language design.
           | Delphi/C#/TypeScript all had tooling built alongside the
           | language and it shows.
           | 
           | Rust sadly made a ton of choices in the language that just
           | make a lot of things hard, but enabled the type of ecosystem
           | that exists today. So it has different tradeoffs. I'm quite
           | amazed how well RLS is doing considering how hostile the
           | language is to fast iteration.
        
             | valenterry wrote:
             | Look at Kotlin to see the opposite. It was designed to be
             | IDE friendly - in fact, it was developed by the IntelliJ
             | folks.
             | 
             | However, looking at it now, it seems that Kotlin is pretty
             | much falling behind even Java in terms of features and
             | functionality. But especially it also seems behind in a
             | well-thought design.
             | 
             | When looking at Rust, I think while there are still a lot
             | of features that I would desire, it at least introduced
             | major improvements like typeclasses (or "traits" in Rust
             | lingo) which really help the ecosystem and libraries to
             | flourish. If this makes it harder for the compiler and the
             | IDE then I think it's a fair tradeoff. Kotlin had the
             | chance to do the same, they deliberately didn't and I is
             | already starting to hinder the language from thriving.
        
               | 4RealFreedom wrote:
               | Curious as to what parts of Kotlin you think are falling
               | behind Java. Java has been playing catch-up forever. Java
               | just implemented light weight threads - coroutines in
               | Kotlin which I've been using for years. I've used data
               | classes forever and, again, Java just recently
               | implemented their equivalent - records. Sealed classes
               | are the only feature I can think of but not a must-have
               | for me. What am I missing? For some background, I've been
               | a Java developer for over 10 years. I work with Kotlin
               | and Java daily - my company has a backend written in
               | Kotlin and many ancillary services in Java. I prefer
               | Kotlin - it's a joy to write in. JetBrains hit a sweet
               | spot by being able to mix functional and imperative
               | coding.
        
               | valenterry wrote:
               | A good example is the latest pattern-matching addendums
               | to Java which are lacking in Kotlin in comparison. Sealed
               | classes (aka coproducts/sumtypes) are another and are an
               | _essential_ part of any programming language, on the same
               | level es enums. I think you are experiencing the blub-
               | paradox in this case.
               | 
               | Yes, Java has been (and is still) playing catch-up, but
               | there has been some serious progress recently and _when_
               | features come, they tend to be better than the Kotlin
               | counterparts from what I can tell.
               | 
               | That being said, I'm not claiming that Java is better
               | than Kotlin right now. But when projecting into the
               | future, I doubt that Kotlin has one, sorry to say that. A
               | couple of years ago I would have said the exact opposite,
               | but Java has really stepped up since then.
        
               | 4RealFreedom wrote:
               | Wait, if sealed classes are essential, how did java
               | survive so long without them? Possibly a nice-to-have but
               | essential? To say Kotlin doesn't have a future when it is
               | one of the widest used languages is a stretch. I think
               | it's great that Java has been stepping up their feature
               | development. I will continue to use both.
        
               | valenterry wrote:
               | Well, java "survived" without it because, at the time,
               | most other languages didn't have sumtypes either. But
               | almost every new/modern language does. Exceptions are
               | rare (golang is one of them and even for golang there are
               | discussions: https://github.com/golang/go/issues/57644)
               | 
               | > To say Kotlin doesn't have a future when it is one of
               | the widest used languages is a stretch
               | 
               | Well, let me say it like that: if Java and Kotlin
               | continue to progress with the same speed like they do
               | now, then Kotlin will not be a "better Java" anymore and
               | the only selling-point will be IDE support (if that's
               | even possible).
        
               | rhdunn wrote:
               | Kotlin is in a tricky place w.r.t. Java because the JVM
               | is one of Kotlin's target platforms. If Kotlin deviated
               | from the JVM too far, it would be harder to migrate to
               | the newer JVM versions.
               | 
               | That specifically applies to things like pattern matching
               | and value classes. Value classes are constrained to
               | single values to allow the Kotlin compiler to do the
               | heavy lifting without involving specific JVM
               | capabilities.
               | 
               | It will be interesting to see how Kotlin evolves with
               | version 2.0 and beyond, with the new compiler and stable
               | multiplatform support.
        
               | valenterry wrote:
               | I don't think so. Look at Scala, which is in the same
               | position as Kotlin. Still, it has a far superior
               | featureset both compared to Java and Kotlin and has had
               | pattern matching since almost forever (in fact, Java's
               | pattern matching, sealed classes etc. are heavily
               | inspired by Scala's). And it manages quite fine to do all
               | these things, even before the JVM improved to support
               | those features better in a native way.
        
             | matklad wrote:
             | I am not entirely sure about C#, if we speak about "static
             | analysis based IDE" tooling. I think this only became focus
             | with Roslyn, and Roslyn came later, after JetBrains did
             | their own thing with resharper.
             | 
             | But yeah, directionally we are getting there: Microsoft
             | seems to be the main popularizer of the idea (TypeScript
             | and LSP), and Google seems to be most acutely aware about
             | the "tooling gap" (Go, Dart, and, most recently, Carbon are
             | cognizant of tooling needs. Carbon has signatures for
             | tooling, and Dart shipped a better version of LSP before
             | LSP became a thing).
        
               | the_mitsuhiko wrote:
               | I just remember years ago reading interviews with
               | Hejlsberg about how he designed C# (particularly LINQ)
               | with IDEs in mind. Given that he also worked on Delphi I
               | think it's a core pillar.
        
               | hawk_ wrote:
               | I think that was meant in a very narrow sense comparing
               | to SQL. In SQL the columns being selected go before the
               | the table name so the IDE can't suggest a set of valid
               | columns. In LINQ the order is flipped. But other than
               | that there's no grand language design there to support
               | tooling.
        
               | the_mitsuhiko wrote:
               | You don't have to have a grand design, you just need to
               | get the important pieces right. C# has an excellent IDE
               | experience not because LINQ is in the right order but
               | because it has a fundamental compilation model and object
               | system that supports tooling well. They could have gone
               | nuts with compile time logic but they did not to support
               | a better IDE experience.
        
         | eptcyka wrote:
         | Designing syntax and semantics that are easier to parse can
         | also make the language easier to understand to humans, as long
         | as optimizing for these goals doesn't impede on useful language
         | features.
        
         | levodelellis wrote:
         | I wrote my (prototype) LSP in a completely different way from
         | the article and I got very good results (autocomplete took less
         | than 10ms, IIRC was <5). I didn't intentionally design my
         | language for it but I did have to write my compiler in a way
         | that would keep the data around so I can support it
        
       ___________________________________________________________________
       (page generated 2023-06-10 23:02 UTC)