<p>I'm currently designing a language with a heavy focus on compile-time code execution. In particular, all types will be handled as regular compile-time values (similar to the Zig language). This implies a powerful reflection system. Another important detail is that compile-time code may have side-effects, both incoming (like reading files, querying time, randomizing and so on) and outgoing (writing build logs, building documentation... In fact, building the program itself will be implemented as part of the compile-time code).</p>
<p>The question is, how could an LSP be organised for such a language? My best idea so far is to simply trigger the compilation and record the state from within, however that might have the undesirable side effect of producing all of the build's side effects.</p>
<p>Python, on the surface, seems to have the same issue but to a much greater scale. It has dynamic type information and very flexible reflection involved, and the LSP cannot execute the code because it could have significant side effects or require user input. From my experience, this seems to limit Python IDEs to only recognizing simple constructs without reflection, and struggling with dynamic attributes, for example. However, if there are more robust options for it, the same principles might be applicable to my case. If you know of any, I'm interested in that too.</p>

<language-server-protocol><constant-expressions><side-effects>
LSP for a language with a lot of compile-time execution
CC BY-SA 4.0
2023-07-18T08:21:42.700Z
You just have to accept that sometimes your LSP feedback will be slower than for the more primitive languages. It's ok and does not really bother me. Also, you can use purity analysis to aggressively memoise the execution results, and only re-run things that changed.
SK-logic
<h2>Reproducible builds + heavy caching</h2>
<h3>What are reproducible builds</h3>
<p>Imagine you need to run an open source program that's available in your distro's package repository as a binary. How can you trust that the source you see corresponds to the binary you download, and has not been modified (maliciously or by mistake)? Enter <a href="https://en.wikipedia.org/wiki/Reproducible_builds" rel="nofollow noreferrer">Reproducible Builds</a>.</p>
<p>For mainstream languages, this means having a compiler and build process that is deterministic and has fixed inputs: from link order to timestamps, everything should be either stable between environments, or accept explicit values as inputs. This way anybody can build the program from source and verify it matches the binary published.</p>
<h2>Caching in the LSP</h2>
<p>I'd group compile-time code execution side effects in three general categories:</p>
<ul>
<li>Inputs: data files, confirmation dialogs, downloading dependencies.</li>
<li>Time and randomness: recording timestamps, <a href="https://en.wikipedia.org/wiki/Address_space_layout_randomization" rel="nofollow noreferrer">ASLR</a>, <a href="https://lwn.net/Articles/474912/" rel="nofollow noreferrer">randomizing hash tables</a>.</li>
<li>Outputs: logs, generating documentation, exporting schemas, uploading snapshots.</li>
</ul>
<p>You'll notice that most of them are common problems to all reproducible builds, just your &quot;compiler&quot; also includes some of the user code. And with that separation, you can pick different strategies:</p>
<ul>
<li>Inputs: cache them heavily. Files can be identified by path+modification time, or path+permissions+hash. Downloads can be cached for the session, or with a max age. Leave the rest, like confirmation dialogs, up to the user.</li>
<li>Time and randomness: get a timestamp and random seed at LSP startup, and reuse them for every build.</li>
<li>Outputs: provide a language-level flag to differentiate normal builds from LSP, and emit warnings for functions that output during LSP builds. Then it's up to the user to avoid them.</li>
</ul>
<hr />
<p>This is a compromise solution, as it's still slower than a completely static language and will not cover 100% of compile-time code execution use cases. But I feel like it's a good mid-point between forbidding the feature (and being unusable for most non-trivial projects) and mysteriously slow LSP responses.</p>
<p>For an extreme example of what can be cached, see the <a href="https://rr-project.org/" rel="nofollow noreferrer">rr debugger</a> that records everything down to the context-switch timings, enabling time travel debugging.</p>

CC BY-SA 4.0
2023-07-18T10:09:04.333Z
2023-07-18T14:18:28.050Z
2023-07-18T14:18:28.050Z
BoppreH
# LSP for a language with a lot of compile-time execution
Posted by: username_0 on 2023-07-17
I'm currently designing a language with a heavy focus on compile-time code execution. In particular, all types will be handled as regular compile-time values (similar to the Zig language). This implies a powerful reflection system. Another important detail is that compile-time code may have side-effects, both incoming (like reading files, querying time, randomizing and so on) and outgoing (writing build logs, building documentation... In fact, building the program itself will be implemented as part of the compile-time code).

The question is, how could an LSP be organised for such a language? My best idea so far is to simply trigger the compilation and record the state from within, however that might have the undesirable side effect of producing all of the build's side effects.

Python, on the surface, seems to have the same issue but to a much greater scale. It has dynamic type information and very flexible reflection involved, and the LSP cannot execute the code because it could have significant side effects or require user input. From my experience, this seems to limit Python IDEs to only recognizing simple constructs without reflection, and struggling with dynamic attributes, for example. However, if there are more robust options for it, the same principles might be applicable to my case. If you know of any, I'm interested in that too.
**Tags:** language-server-protocol constant-expressions side-effects
> **Comments:**
> **username_1**: You just have to accept that sometimes your LSP feedback will be slower than for the more primitive languages. It's ok and does not really bother me. Also, you can use purity analysis to aggressively memoise the execution results, and only re-run things that changed.  
---
## 1 answers
### Answer by username_2 on 2023-07-18. Score: 5 | Accepted answer
## Reproducible builds + heavy caching

### What are reproducible builds

Imagine you need to run an open source program that's available in your distro's package repository as a binary. How can you trust that the source you see corresponds to the binary you download, and has not been modified (maliciously or by mistake)? Enter [Reproducible Builds](https://en.wikipedia.org/wiki/Reproducible_builds).

For mainstream languages, this means having a compiler and build process that is deterministic and has fixed inputs: from link order to timestamps, everything should be either stable between environments, or accept explicit values as inputs. This way anybody can build the program from source and verify it matches the binary published.

## Caching in the LSP

I'd group compile-time code execution side effects in three general categories:

* Inputs: data files, confirmation dialogs, downloading dependencies.
* Time and randomness: recording timestamps, [ASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization), [randomizing hash tables](https://lwn.net/Articles/474912/).
* Outputs: logs, generating documentation, exporting schemas, uploading snapshots.

You'll notice that most of them are common problems to all reproducible builds, just your "compiler" also includes some of the user code. And with that separation, you can pick different strategies:

* Inputs: cache them heavily. Files can be identified by path+modification time, or path+permissions+hash. Downloads can be cached for the session, or with a max age. Leave the rest, like confirmation dialogs, up to the user.
* Time and randomness: get a timestamp and random seed at LSP startup, and reuse them for every build.
* Outputs: provide a language-level flag to differentiate normal builds from LSP, and emit warnings for functions that output during LSP builds. Then it's up to the user to avoid them.

---

This is a compromise solution, as it's still slower than a completely static language and will not cover 100% of compile-time code execution use cases. But I feel like it's a good mid-point between forbidding the feature (and being unusable for most non-trivial projects) and mysteriously slow LSP responses.

For an extreme example of what can be cached, see the [rr debugger](https://rr-project.org/) that records everything down to the context-switch timings, enabling time travel debugging.
---
