What if we included the language server directly in the compiler binary under the roc lsp command? That way there's only one binary to install, you automatically have the language server ready to go, and the language server version always matches that of the cli. This seems like a really nice user experience to me
After searching, Deno already does this
I know Richard has said that he wanted to avoid doing this because he didn't want to block LSP improvements behind a full compiler release.
I think that's a good idea when releases are slow, for example the Gleam compiler is getting new LSP features in every release, but I don't get them until my package manager updates Gleam and I can be bothered to upgrade each project to use the new Gleam version. Roc isn't at that stable stage yet, so it might not be a bad idea to bundle them for now. :+1:
yeah I want to do this like roc glue
where instead of roc lsp it's like roc tooling https://example.com/roc-lsp/abcdfg123a.tar.gz
so you provide a roc script that can be lsp or whatever else - we just expose to it (much like roc glue) with all the inputs it needs to do its job
so that way whoever is implementing the language server extension for a given editor can use whatever lsp.roc (or whatever) along with the user's local roc binary, and those can be independent
also, cards on the table, some of my design goals for the roc binary include:
roc.json file ever), or for that matter other configuration file formats like TOMLHey that’s a cool idea!
It seems like the interface that would need to be exposed by the compiler to support building a rich LSP could be large. Having to deal with maintaining that interface and not breaking it all the time sounds like it could be difficult.
That’s one advantage of embedding the language server, then you don’t have to expose and maintain any kind of public interface like that
But I totally see the appeal of keeping json and lsp out of the compiler
And supporting other interesting tooling to be created
I don't think it's actually much more difficult honestly
for example, I think it's fine if we do something like exposing essentially all the functions the language server wants access to, and then the .roc file's job is basically just to run a little server and call those functions and send the results as JSON across the server
I'm more thinking of difficulty in terms of dealing with backwards compatibility and breaking changes. I.e. if something changes about how the compiler works internally that impacts the interface and now we have to worry about either breaking all of the tooling built on those APIs or somehow maintaining the original interface
Whereas if we only exposed an LSP, it would be the only client that we'd need to update and their wouldn't be an exposed API
It does sound worth it to have the ability for non-lsp tooling to be built though so I'm not saying this is a reason to not go with that approach
ah I see - I think the same consideration is true of glue though
Yes for sure
I honestly think the right way to do this is to expose some sort of sexpr like API. Just give the roc script the raw data in a flexible form.
This way the API is simple but exposes a lot
Than leave the roc script to do whatever it wants
but it also breaks every time we change our internal representation :sweat_smile:
Could we provide a roc library that wraps that sexpr API and keep it in sync with our internal representation?
It would be useful for glue and other code generation things, LSP etc.
The alternative is that the platform abstraction we provide for tooling scripts would be need to be more detailed. Like instead of just exposing nodes of a few types, we would need to expose a representation for expressions, statements, types etc.
Also, we're pretty much there already with the sexpr thing, since we are using that for our snapshot tests.
The Parser SExpr's feel pretty mature and I could imagine using something like that to code-gen roc source. Here's the current snapshot for hello-world.
(file (1:1-5:42)
(app (1:1-1:57)
(provides (1:6-1:12) (exposed_item (lower_ident "main!")))
(record_field (1:15-1:57)
"pf"
(string (1:28-1:55) (string_part (1:29-1:54) "../basic-cli/platform.roc")))
(packages (1:13-1:57)
(record_field (1:15-1:57)
"pf"
(string (1:28-1:55) (string_part (1:29-1:54) "../basic-cli/platform.roc")))))
(statements
(import (3:1-3:17) ".Stdout" (qualifier "pf"))
(decl (5:1-5:42)
(ident (5:1-5:6) "main!")
(lambda (5:9-5:42)
(args (underscore))
(apply (5:13-5:42)
(ident (5:13-5:25) "Stdout" ".line!")
(string (5:26-5:41) (string_part (5:27-5:40) "Hello, world!")))))))
So I imagine a roc library that generates the Parser sexpr nodes and provides some type safety etc, so your not writing raw strings for "file", "statements", "apply" etc.
I think once we have the basics of an interpreter for running simple roc apps it will be easy to try out the simple sexpr API for roc tooling scripts.
Yeah, I want to essentially expose what Luke shared there
And yes, if roc updates it make break the lsp, but that is correct behaviour. If the lsp cares about specific mode types and roc changes nodes, that means that the language itself is actually changed....so the lsp needs to update.
In practice, I expect things to be mostly stable.
If the roc compiler understanding of roc the language changes, surely the lsp understanding of roc the language must change too, right?
tooling provides complete information about the file.pf: platform tooling?roc experimental-tooling would be a good name for now?I assumed
toolingprovides complete information about the file.
We can provide the information as people request it (on zulip), I don't think we need to provide everything.
Last updated: Jun 16 2026 at 16:19 UTC