restructure: move el compiler content into lang/

This commit is contained in:
Will Anderson
2026-05-05 01:38:51 -05:00
parent ce68f91a38
commit 1ae68962cf
143 changed files with 0 additions and 0 deletions
+198
View File
@@ -0,0 +1,198 @@
# El LSP — Language Server for El
Full Language Server Protocol implementation for the El programming language.
## Features
| Feature | Status |
|---------|--------|
| Syntax highlighting | Full TextMate grammar |
| Completions | Builtins (130+), user-defined fns, keywords, types |
| Hover | Signatures + descriptions for all builtins and user fns |
| Go-to-definition | Jump to `fn name(` in the open document |
| Diagnostics | Unclosed braces/parens/brackets, unterminated strings |
| Document sync | Full (re-sends entire document on every change) |
## Building
### Prerequisites
- The `elc` compiler binary at `dist/platform/elc` (built from the repo root)
- `cc` (clang or gcc), `libcurl`, `pthreads`
### Build
```bash
# From the el repo root:
./tools/lsp/build.sh
# Or with a custom elc path:
ELC=/path/to/elc ./tools/lsp/build.sh
```
Output: `tools/lsp/dist/el-lsp`
### Install system-wide
```bash
sudo cp tools/lsp/dist/el-lsp /usr/local/bin/el-lsp
```
## VSCode Extension
### Development install (recommended)
1. Build the binary first (see above).
2. Install the npm dependency:
```bash
cd tools/lsp/vscode-extension
npm install
```
3. Open `tools/lsp/vscode-extension/` in VSCode.
4. Press **F5** — this launches the Extension Development Host.
5. Open any `.el` file in the dev host window.
### Package as .vsix
```bash
npm install -g @vscode/vsce
cd tools/lsp/vscode-extension
vsce package
# Produces: el-language-1.0.0.vsix
```
Install the .vsix:
```bash
code --install-extension el-language-1.0.0.vsix
```
### Configuration
| Setting | Default | Description |
|---------|---------|-------------|
| `el.lspPath` | (bundled) | Path to `el-lsp` binary. Empty = use `../dist/el-lsp`. |
| `el.trace.server` | `off` | LSP message tracing. Set to `verbose` to see all messages. |
## Neovim / other editors
Any editor that supports LSP can use `el-lsp`. Example configuration for
Neovim with `nvim-lspconfig`:
```lua
local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')
if not configs.el then
configs.el = {
default_config = {
cmd = { 'el-lsp' },
filetypes = { 'el' },
root_dir = lspconfig.util.root_pattern('.git', '*.el'),
settings = {},
},
}
end
lspconfig.el.setup({})
```
Add to `ftdetect/el.vim`:
```vim
au BufRead,BufNewFile *.el set filetype=el
```
## Wire protocol
El LSP communicates over stdin/stdout using standard LSP framing:
```
Content-Length: <N>\r\n
\r\n
<N bytes of UTF-8 JSON>
```
The `__read_n(n: Int) -> String` primitive in `el_runtime.c` reads exactly
`n` bytes from stdin — needed because `readline()` stops at `\n` and LSP
JSON bodies are not newline-terminated. `__print_raw(s: String)` writes with
`fwrite + fflush` to preserve embedded `\r\n` in headers.
## Smoke test
After building, verify the server responds to `initialize`:
```bash
python3 - << 'PY'
import subprocess, json
def frame(obj):
body = json.dumps(obj).encode()
return f"Content-Length: {len(body)}\r\n\r\n".encode() + body
def read_response(proc):
hdr = b""
while not hdr.endswith(b"\r\n\r\n"):
hdr += proc.stdout.read(1)
cl = int([l for l in hdr.decode().split("\r\n") if "Content-Length" in l][0].split(": ")[1])
return json.loads(proc.stdout.read(cl))
proc = subprocess.Popen(["./dist/el-lsp"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
proc.stdin.write(frame({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}))
proc.stdin.flush()
r = read_response(proc)
print("Server name:", r["result"]["serverInfo"]["name"])
print("Capabilities:", list(r["result"]["capabilities"].keys()))
proc.stdin.write(frame({"jsonrpc":"2.0","method":"exit","params":{}}))
proc.stdin.flush()
proc.wait()
print("OK")
PY
```
## Architecture
```
el-lsp.el
lsp_read_message() reads header bytes one-by-one, then __read_n(body_len)
lsp_write_message() __print_raw("Content-Length: N\r\n\r\n" + json)
lsp_dispatch() routes method string to handler
lsp_builtin_catalog() [String] of "name|signature|description" entries
lsp_extract_fns() scan source for "fn name(" patterns
lsp_word_at() expand identifier under cursor
lsp_compute_diagnostics() scan for unclosed brackets + unterminated strings
```
## Files
```
tools/lsp/
el-lsp.el LSP server source (El language)
build.sh Build script
README.md This file
dist/
el-lsp Compiled binary (after build)
el-lsp.c Generated C (after build)
vscode-extension/
extension.js Extension entry point
package.json Extension manifest
language-configuration.json Bracket matching, comment config
syntaxes/
el.tmGrammar.json TextMate syntax grammar
.vscode/
launch.json F5 debug configuration
tasks.json Pre-launch npm install task
```
## Runtime additions
Two new primitives added to `el-compiler/runtime/`:
### `__read_n(n: Int) -> String`
Reads exactly `n` bytes from stdin using `fread`. Returns `""` on EOF.
Required for reading JSON-RPC message bodies.
### `__print_raw(s: String) -> Void`
Writes a string to stdout using `fwrite + fflush`. Preserves embedded
`\r\n` bytes exactly. Required for LSP Content-Length headers.
Both are declared in `el_runtime.h` and registered in the `builtin_arity`
table in `el-compiler/src/codegen.el`.
Vendored Executable
BIN
View File
Binary file not shown.
+1252
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Extension (Extension Development Host)",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/**/*.js"
],
"preLaunchTask": "npm: install",
"env": {
"EL_LSP_LOG": "1"
}
},
{
"name": "Attach to el-lsp process",
"type": "node",
"request": "attach",
"port": 6009,
"restart": true,
"timeout": 10000,
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}
+16
View File
@@ -0,0 +1,16 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "install",
"label": "npm: install",
"detail": "Install vscode-languageclient dependency",
"group": "build",
"presentation": {
"reveal": "silent"
},
"problemMatcher": []
}
]
}
@@ -0,0 +1,150 @@
// tools/lsp/vscode-extension/extension.js
//
// El Language VSCode extension — Language Client
//
// Launches the el-lsp binary as a child process and connects VSCode's
// Language Client to it over stdin/stdout (JSON-RPC 2.0 + Content-Length).
//
// Capabilities provided by el-lsp:
// - textDocumentSync (full)
// - completionProvider — builtins + user fns + keywords
// - hoverProvider — signatures + descriptions
// - definitionProvider — go-to-def for user-defined fns
// - diagnostics — unclosed braces/parens, unterminated strings
//
// Setup (development):
// 1. cd tools/lsp && ./build.sh (builds dist/el-lsp)
// 2. cd vscode-extension && npm install (installs vscode-languageclient)
// 3. Open vscode-extension/ in VSCode
// 4. Press F5 (launches Extension Development Host)
// 5. Open any .el file
//
// Package as .vsix:
// npm install -g @vscode/vsce
// cd tools/lsp/vscode-extension && vsce package
'use strict';
const path = require('path');
const fs = require('fs');
const { workspace, window, ExtensionContext } = require('vscode');
const {
LanguageClient,
TransportKind,
} = require('vscode-languageclient/node');
/** @type {LanguageClient | undefined} */
let client;
/**
* activate — entry point called when any .el file is first opened.
*
* @param {ExtensionContext} context
*/
function activate(context) {
// ── Resolve el-lsp binary ────────────────────────────────────────────
const config = workspace.getConfiguration('el');
// Default path resolution (tries in order):
// 1. el.lspPath setting (user override)
// 2. dist/el-lsp alongside the extension dir (packaged install)
// 3. The canonical source-tree location
const CANONICAL_LSP = '/Users/will/Development/neuron-technologies/foundation/el/tools/lsp/dist/el-lsp';
const defaultLspPath = (() => {
const sibling = path.join(context.extensionPath, 'dist', 'el-lsp');
if (fs.existsSync(sibling)) return sibling;
return CANONICAL_LSP;
})();
const serverPath = config.get('lspPath') || defaultLspPath;
if (!fs.existsSync(serverPath)) {
window.showErrorMessage(
`El Language Server binary not found at: ${serverPath}\n` +
`Run tools/lsp/build.sh to build it, then reload VSCode.\n` +
`Or set "el.lspPath" in settings to the correct path.`
);
return;
}
// ── Server options (start el-lsp as child process) ───────────────────
/** @type {import('vscode-languageclient/node').ServerOptions} */
const serverOptions = {
run: {
command: serverPath,
transport: TransportKind.stdio,
options: { env: { ...process.env } },
},
debug: {
command: serverPath,
transport: TransportKind.stdio,
options: {
env: { ...process.env },
// Redirect el-lsp stderr to VSCode's Output panel in debug mode.
},
},
};
// ── Client options ───────────────────────────────────────────────────
/** @type {import('vscode-languageclient/node').LanguageClientOptions} */
const clientOptions = {
// Activate for all .el files (file:// and untitled: schemes).
documentSelector: [
{ scheme: 'file', language: 'el' },
{ scheme: 'untitled', language: 'el' },
],
synchronize: {
// Fire fileEvents so the client resends on disk changes outside VSCode.
fileEvents: workspace.createFileSystemWatcher('**/*.el'),
},
outputChannelName: 'El Language Server',
// Trace LSP messages to the Output panel for debugging.
// Set "el.trace.server": "verbose" in settings to enable.
traceOutputChannel: window.createOutputChannel('El LSP Trace'),
};
// ── Create and start client ──────────────────────────────────────────
client = new LanguageClient(
'el-language-server',
'El Language Server',
serverOptions,
clientOptions,
);
// Register the client so it is disposed when the extension deactivates.
context.subscriptions.push(client);
client.start().then(() => {
// Show a discrete status bar item while the server is active.
const status = window.createStatusBarItem(1);
status.text = '$(symbol-misc) El LSP';
status.tooltip = 'El Language Server is running';
status.command = 'el.restartServer';
status.show();
context.subscriptions.push(status);
}).catch((err) => {
window.showErrorMessage(`El Language Server failed to start: ${err.message}`);
});
// ── Commands ─────────────────────────────────────────────────────────
context.subscriptions.push(
require('vscode').commands.registerCommand('el.restartServer', () => {
if (client) {
client.stop().then(() => client.start());
window.showInformationMessage('El Language Server restarted.');
}
})
);
}
/**
* deactivate — called when the extension is unloaded.
* Sends LSP shutdown + exit to the server process.
*/
function deactivate() {
if (!client) {
return undefined;
}
return client.stop();
}
module.exports = { activate, deactivate };
@@ -0,0 +1,26 @@
{
"comments": {
"lineComment": "//"
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"", "notIn": ["string"] }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""]
],
"indentationRules": {
"increaseIndentPattern": "\\{[^}]*$",
"decreaseIndentPattern": "^\\s*}"
}
}
@@ -0,0 +1,93 @@
{
"name": "el-language",
"displayName": "El Language",
"description": "El language support \u2014 syntax highlighting, completions, hover, go-to-definition, and diagnostics",
"version": "1.0.0",
"publisher": "neuron-technologies",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/neuron-technologies/foundation"
},
"engines": {
"vscode": "^1.75.0"
},
"categories": [
"Programming Languages",
"Linters",
"Other"
],
"keywords": [
"el",
"el-lang",
"language-server",
"lsp",
"neuron"
],
"activationEvents": [
"onLanguage:el"
],
"contributes": {
"languages": [
{
"id": "el",
"aliases": [
"El",
"el-lang"
],
"extensions": [
".el",
".elh"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "el",
"scopeName": "source.el",
"path": "./syntaxes/el.tmGrammar.json"
}
],
"configuration": {
"type": "object",
"title": "El Language",
"properties": {
"el.lspPath": {
"type": "string",
"default": "",
"description": "Absolute path to the el-lsp binary. Leave empty to use the binary bundled with the extension (../dist/el-lsp)."
},
"el.trace.server": {
"type": "string",
"enum": [
"off",
"messages",
"verbose"
],
"default": "off",
"description": "Trace LSP messages between VSCode and el-lsp (visible in Output \u2192 El LSP Trace)."
}
}
},
"commands": [
{
"command": "el.restartServer",
"title": "El: Restart Language Server"
}
]
},
"main": "./extension.js",
"dependencies": {
"vscode-languageclient": "^8.1.0"
},
"devDependencies": {
"@types/vscode": "^1.75.0",
"@vscode/vsce": "^2.22.0"
},
"scripts": {
"vscode:prepublish": "echo 'no transpile step required'",
"package": "vsce package",
"install-dev": "npm install && code --install-extension el-language-1.0.0.vsix 2>/dev/null || true"
}
}
@@ -0,0 +1,175 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "El",
"scopeName": "source.el",
"fileTypes": ["el"],
"patterns": [
{ "include": "#comments" },
{ "include": "#strings" },
{ "include": "#function-definition" },
{ "include": "#builtin-functions" },
{ "include": "#keywords-control" },
{ "include": "#keywords-declaration" },
{ "include": "#keywords-other" },
{ "include": "#types" },
{ "include": "#type-annotations" },
{ "include": "#constants" },
{ "include": "#numbers" },
{ "include": "#operators" },
{ "include": "#range-operator" },
{ "include": "#function-call" },
{ "include": "#identifiers" }
],
"repository": {
"comments": {
"name": "comment.line.double-slash.el",
"match": "//.*$"
},
"strings": {
"name": "string.quoted.double.el",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.el",
"match": "\\\\[nrt\\\\\"'0]"
},
{
"name": "constant.character.escape.hex.el",
"match": "\\\\x[0-9a-fA-F]{2}"
}
]
},
"function-definition": {
"comment": "fn keyword + function name — name gets entity.name.function",
"match": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()",
"captures": {
"1": { "name": "keyword.declaration.function.el" },
"2": { "name": "entity.name.function.el" }
}
},
"builtin-functions": {
"comment": "Well-known El builtins get support.function scope for distinct colour",
"name": "support.function.builtin.el",
"match": "\\b(println|print|readline|str_eq|str_len|str_concat|str_slice|str_contains|str_starts_with|str_ends_with|str_replace|str_to_upper|str_to_lower|str_trim|str_lstrip|str_rstrip|str_index_of|str_last_index_of|str_split|str_split_lines|str_split_n|str_join|str_char_at|str_char_code|str_pad_left|str_pad_right|str_repeat|str_reverse|str_strip_prefix|str_strip_suffix|str_strip_chars|str_count|str_count_lines|str_find_chars|str_upper|str_lower|int_to_str|str_to_int|float_to_str|str_to_float|int_to_float|float_to_int|format_float|decimal_round|bool_to_str|el_abs|el_max|el_min|math_sqrt|math_log|math_ln|math_sin|math_cos|math_pi|el_list_empty|el_list_append|el_list_len|el_list_get|el_list_clone|list_push|list_push_front|list_join|list_range|el_map_get|el_map_set|el_get_field|state_set|state_get|state_del|state_keys|json_get|json_get_string|json_get_int|json_get_float|json_get_bool|json_get_raw|json_set|json_parse|json_stringify|json_array_len|json_array_get|json_array_get_string|fs_read|fs_write|fs_list|fs_exists|fs_mkdir|http_get|http_post|http_post_json|http_get_with_headers|http_post_with_headers|http_serve|http_serve_v2|http_response|url_encode|url_decode|time_now|time_now_utc|sleep_secs|sleep_ms|time_format|time_add|time_diff|now|unix_seconds|unix_millis|uuid_new|uuid_v4|env|args|exit_program|exec_command|exec_capture|sha256_hex|hmac_sha256_hex|base64_encode|base64_decode|base64url_encode|base64url_decode|llm_call|llm_call_system|llm_call_agentic|llm_vision|llm_models|llm_register_tool|engram_node|engram_get_node|engram_strengthen|engram_forget|engram_search|engram_connect|engram_activate|engram_save|engram_load|engram_node_count|engram_edge_count|engram_neighbors|engram_search_json|engram_stats_json|dharma_connect|dharma_send|dharma_activate|dharma_emit|dharma_field|dharma_peers|native_list_empty|native_list_append|native_list_len|native_list_get|native_list_clone|native_string_chars|native_int_to_str)\\b(?=\\s*\\()"
},
"keywords-control": {
"name": "keyword.control.el",
"match": "\\b(if|else|while|for|in|return|match|break|continue)\\b"
},
"keywords-declaration": {
"name": "keyword.declaration.el",
"match": "\\b(fn|let|type|enum|import|from|as|extern)\\b"
},
"keywords-other": {
"name": "keyword.other.el",
"match": "\\b(cgi|vessel|activate|where|sealed|with|test|seed|assert|protocol|impl|retry|times|fallback|reason|parallel|trace|requires|deploy|to|via|target|manager|engine|accessor)\\b"
},
"types": {
"comment": "Built-in El primitive types",
"name": "support.type.primitive.el",
"match": "\\b(String|Int|Float|Bool|Void|Any|Instant|Duration|Map|List)\\b"
},
"type-annotations": {
"comment": "Highlight type annotations after : in let and fn params",
"patterns": [
{
"match": ":\\s*([A-Z][a-zA-Z0-9_<>, ]*)(?=\\s*[,)={\\]]|\\s*->)",
"captures": {
"1": { "name": "support.type.el" }
}
},
{
"match": "->\\s*([A-Z][a-zA-Z0-9_<>, ]*)",
"captures": {
"0": { "name": "keyword.operator.arrow.el" },
"1": { "name": "support.type.return.el" }
}
}
]
},
"constants": {
"patterns": [
{
"name": "constant.language.boolean.el",
"match": "\\b(true|false)\\b"
},
{
"name": "constant.language.null.el",
"match": "\\bnull\\b"
}
]
},
"numbers": {
"patterns": [
{
"name": "constant.numeric.float.el",
"match": "-?\\b[0-9]+\\.[0-9]+\\b"
},
{
"name": "constant.numeric.integer.el",
"match": "-?\\b[0-9]+\\b"
}
]
},
"range-operator": {
"name": "keyword.operator.range.el",
"match": "\\.\\."
},
"operators": {
"patterns": [
{
"name": "keyword.operator.comparison.el",
"match": "(==|!=|<=|>=)"
},
{
"name": "keyword.operator.comparison.el",
"match": "(?<![<>-])([<>])(?![>=])"
},
{
"name": "keyword.operator.logical.el",
"match": "(&&|\\|\\||!(?!=))"
},
{
"name": "keyword.operator.assignment.el",
"match": "(?<![=!<>])=(?!=)"
},
{
"name": "keyword.operator.arithmetic.el",
"match": "[+\\-*/%]"
},
{
"name": "keyword.operator.arrow.el",
"match": "->"
}
]
},
"function-call": {
"comment": "Function calls: identifier immediately followed by (",
"match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()",
"captures": {
"1": { "name": "entity.name.function.call.el" }
}
},
"identifiers": {
"comment": "Catch-all for remaining identifiers",
"name": "variable.other.el",
"match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b"
}
}
}