// main.el — el-lsp // // Language Server for El. Pure-El port of the el-lsp Rust crate. // // Entry surface (callable from any El program that imports this file): // // lsp_complete(source: String, pos: Int) -> String // JSON array // lsp_hover(source: String, pos: Int) -> String // JSON object or "null" // lsp_diagnostics(source: String) -> String // JSON array // lsp_outline(source: String) -> String // JSON array // lsp_format(source: String) -> String // JSON { content, changed } // lsp_type_graph(source: String) -> String // JSON { nodes, edges } // lsp_definition(source: String, pos: Int) -> String // JSON object or "null" // lsp_references(source: String, pos: Int) -> String // JSON array // lsp_jsonrpc(method: String, params_json: String) -> String // lsp_stdio_loop() -> Void // // Tokenisation reuses the lexer.el idiom (chars list + index walk) for the // pieces we need (identifier scan, string scan, line counting). The full // type checker referenced by the Rust crate is not yet available in El, so // hover/completion/diagnostics fall back to source-scan heuristics over the // declared types/enums/functions in the file. The wire shape is preserved. // ── Char helpers (mirrors lexer.el) ────────────────────────────────────────── fn is_digit_ch(ch: String) -> Bool { if ch == "0" { return true } if ch == "1" { return true } if ch == "2" { return true } if ch == "3" { return true } if ch == "4" { return true } if ch == "5" { return true } if ch == "6" { return true } if ch == "7" { return true } if ch == "8" { return true } if ch == "9" { return true } false } fn is_alpha_ch(ch: String) -> Bool { let lower: String = str_to_lower(ch) if str_eq(ch, "") { return false } if !str_eq(lower, str_to_upper(lower)) { return true } // letters differ in case // covers a-z (lower-only) — uppercase already handled by inverse case test above if str_contains("abcdefghijklmnopqrstuvwxyz", ch) { return true } if str_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZ", ch) { return true } false } fn is_ident_ch(ch: String) -> Bool { if is_digit_ch(ch) { return true } if is_alpha_ch(ch) { return true } if ch == "_" { return true } false } // ── Source navigation ──────────────────────────────────────────────────────── // Extract the identifier fragment immediately before `pos` (cursor). fn extract_prefix(source: String, pos: Int) -> String { let n: Int = str_len(source) let end: Int = pos if end > n { let end = n } let start: Int = end let chars: [String] = native_string_chars(source) let total: Int = native_list_len(chars) let scanning: Bool = true while scanning { if start <= 0 { let scanning = false } else { let prev: String = native_list_get(chars, start - 1) if is_ident_ch(prev) { let start = start - 1 } else { let scanning = false } } } str_slice(source, start, end) } // Extract the full token (alphanumerics + underscore) surrounding `pos`. fn extract_token(source: String, pos: Int) -> String { let n: Int = str_len(source) if pos > n { return "" } let chars: [String] = native_string_chars(source) let total: Int = native_list_len(chars) let start: Int = pos let go_left: Bool = true while go_left { if start <= 0 { let go_left = false } else { let prev: String = native_list_get(chars, start - 1) if is_ident_ch(prev) { let start = start - 1 } else { let go_left = false } } } let end: Int = pos let go_right: Bool = true while go_right { if end >= total { let go_right = false } else { let cur: String = native_list_get(chars, end) if is_ident_ch(cur) { let end = end + 1 } else { let go_right = false } } } str_slice(source, start, end) } // Translate a byte offset into 1-based (line, col). fn pos_to_line_col(source: String, pos: Int) -> Map { let n: Int = str_len(source) let cap: Int = pos if cap > n { let cap = n } let chars: [String] = native_string_chars(source) let i: Int = 0 let line: Int = 1 let col: Int = 1 while i < cap { let ch: String = native_list_get(chars, i) if ch == "\n" { let line = line + 1 let col = 1 } else { let col = col + 1 } let i = i + 1 } { "line": line, "col": col } } // ── JSON encoding helpers ──────────────────────────────────────────────────── // Escape a string for JSON. Handles ", \, newline, tab. fn json_escape(s: String) -> String { let chars: [String] = native_string_chars(s) let n: Int = native_list_len(chars) let out: String = "" let i: Int = 0 while i < n { let ch: String = native_list_get(chars, i) if ch == "\"" { let out = out + "\\\"" } else { if ch == "\\" { let out = out + "\\\\" } else { if ch == "\n" { let out = out + "\\n" } else { if ch == "\r" { let out = out + "\\r" } else { if ch == "\t" { let out = out + "\\t" } else { let out = out + ch } } } } } let i = i + 1 } out } fn json_str(s: String) -> String { "\"" + json_escape(s) + "\"" } fn json_kv_str(k: String, v: String) -> String { json_str(k) + ":" + json_str(v) } fn json_kv_int(k: String, v: Int) -> String { json_str(k) + ":" + int_to_str(v) } fn json_kv_float(k: String, v: Float) -> String { json_str(k) + ":" + float_to_str(v) } fn json_kv_raw(k: String, raw_v: String) -> String { json_str(k) + ":" + raw_v } // Join a list of pre-encoded JSON strings into a JSON array. fn json_array_of(items: [String]) -> String { let n: Int = native_list_len(items) let out: String = "[" let i: Int = 0 while i < n { if i > 0 { let out = out + "," } let out = out + native_list_get(items, i) let i = i + 1 } out + "]" } // ── Keyword/builtin tables ─────────────────────────────────────────────────── fn keywords_list() -> [String] { let ks: [String] = native_list_empty() let ks = native_list_append(ks, "let") let ks = native_list_append(ks, "fn") let ks = native_list_append(ks, "type") let ks = native_list_append(ks, "enum") let ks = native_list_append(ks, "match") let ks = native_list_append(ks, "return") let ks = native_list_append(ks, "activate") let ks = native_list_append(ks, "where") let ks = native_list_append(ks, "sealed") let ks = native_list_append(ks, "if") let ks = native_list_append(ks, "else") let ks = native_list_append(ks, "for") let ks = native_list_append(ks, "in") let ks = native_list_append(ks, "while") let ks = native_list_append(ks, "true") let ks = native_list_append(ks, "false") let ks = native_list_append(ks, "test") let ks = native_list_append(ks, "seed") let ks = native_list_append(ks, "assert") let ks = native_list_append(ks, "target") let ks = native_list_append(ks, "protocol") let ks = native_list_append(ks, "impl") let ks = native_list_append(ks, "import") let ks = native_list_append(ks, "from") let ks = native_list_append(ks, "as") let ks = native_list_append(ks, "with") let ks = native_list_append(ks, "retry") let ks = native_list_append(ks, "times") let ks = native_list_append(ks, "fallback") let ks = native_list_append(ks, "reason") let ks = native_list_append(ks, "parallel") let ks = native_list_append(ks, "trace") let ks = native_list_append(ks, "requires") let ks = native_list_append(ks, "deploy") let ks = native_list_append(ks, "to") let ks = native_list_append(ks, "via") let ks = native_list_append(ks, "vessel") let ks = native_list_append(ks, "cgi") ks } // Built-in primitive type names. fn builtin_type_names() -> [String] { let ts: [String] = native_list_empty() let ts = native_list_append(ts, "Int") let ts = native_list_append(ts, "Float") let ts = native_list_append(ts, "String") let ts = native_list_append(ts, "Bool") let ts = native_list_append(ts, "Uuid") let ts = native_list_append(ts, "Void") let ts = native_list_append(ts, "Any") ts } fn builtin_type_doc(name: String) -> String { if str_eq(name, "Int") { return "64-bit signed integer" } if str_eq(name, "Float") { return "64-bit IEEE 754 double" } if str_eq(name, "String") { return "UTF-8 string" } if str_eq(name, "Bool") { return "Boolean value" } if str_eq(name, "Uuid") { return "RFC 4122 UUID" } if str_eq(name, "Void") { return "Unit type — no value" } if str_eq(name, "Any") { return "Dynamically-typed value" } "" } // Built-in function names → signatures. Encoded as flat [name, sig, name, sig, ...] // to keep type maps simple. fn builtin_fns() -> [String] { let xs: [String] = native_list_empty() let xs = native_list_append(xs, "println") let xs = native_list_append(xs, "fn(value: String) -> Void") let xs = native_list_append(xs, "print") let xs = native_list_append(xs, "fn(value: String) -> Void") let xs = native_list_append(xs, "readline") let xs = native_list_append(xs, "fn() -> String") let xs = native_list_append(xs, "args") let xs = native_list_append(xs, "fn() -> [String]") let xs = native_list_append(xs, "int_to_str") let xs = native_list_append(xs, "fn(n: Int) -> String") let xs = native_list_append(xs, "str_to_int") let xs = native_list_append(xs, "fn(s: String) -> Int") let xs = native_list_append(xs, "str_len") let xs = native_list_append(xs, "fn(s: String) -> Int") let xs = native_list_append(xs, "str_slice") let xs = native_list_append(xs, "fn(s: String, start: Int, end: Int) -> String") let xs = native_list_append(xs, "str_concat") let xs = native_list_append(xs, "fn(a: String, b: String) -> String") let xs = native_list_append(xs, "str_contains") let xs = native_list_append(xs, "fn(s: String, sub: String) -> Bool") let xs = native_list_append(xs, "str_starts_with") let xs = native_list_append(xs, "fn(s: String, p: String) -> Bool") let xs = native_list_append(xs, "str_ends_with") let xs = native_list_append(xs, "fn(s: String, suf: String) -> Bool") let xs = native_list_append(xs, "str_to_upper") let xs = native_list_append(xs, "fn(s: String) -> String") let xs = native_list_append(xs, "str_to_lower") let xs = native_list_append(xs, "fn(s: String) -> String") let xs = native_list_append(xs, "str_split") let xs = native_list_append(xs, "fn(s: String, sep: String) -> [String]") let xs = native_list_append(xs, "str_trim") let xs = native_list_append(xs, "fn(s: String) -> String") let xs = native_list_append(xs, "str_replace") let xs = native_list_append(xs, "fn(s: String, from: String, to: String) -> String") let xs = native_list_append(xs, "str_index_of") let xs = native_list_append(xs, "fn(s: String, sub: String) -> Int") let xs = native_list_append(xs, "float_to_str") let xs = native_list_append(xs, "fn(f: Float) -> String") let xs = native_list_append(xs, "str_to_float") let xs = native_list_append(xs, "fn(s: String) -> Float") let xs = native_list_append(xs, "math_sqrt") let xs = native_list_append(xs, "fn(n: Float) -> Float") let xs = native_list_append(xs, "math_log") let xs = native_list_append(xs, "fn(n: Float) -> Float") let xs = native_list_append(xs, "math_sin") let xs = native_list_append(xs, "fn(n: Float) -> Float") let xs = native_list_append(xs, "math_cos") let xs = native_list_append(xs, "fn(n: Float) -> Float") let xs = native_list_append(xs, "math_pi") let xs = native_list_append(xs, "fn() -> Float") let xs = native_list_append(xs, "el_abs") let xs = native_list_append(xs, "fn(n: Int) -> Int") let xs = native_list_append(xs, "el_min") let xs = native_list_append(xs, "fn(a: Int, b: Int) -> Int") let xs = native_list_append(xs, "el_max") let xs = native_list_append(xs, "fn(a: Int, b: Int) -> Int") let xs = native_list_append(xs, "el_list_empty") let xs = native_list_append(xs, "fn() -> [Any]") let xs = native_list_append(xs, "el_list_len") let xs = native_list_append(xs, "fn(l: [Any]) -> Int") let xs = native_list_append(xs, "el_list_get") let xs = native_list_append(xs, "fn(l: [Any], i: Int) -> Any") let xs = native_list_append(xs, "el_list_append") let xs = native_list_append(xs, "fn(l: [Any], v: Any) -> [Any]") let xs = native_list_append(xs, "list_join") let xs = native_list_append(xs, "fn(l: [String], sep: String) -> String") let xs = native_list_append(xs, "list_range") let xs = native_list_append(xs, "fn(start: Int, end: Int) -> [Int]") let xs = native_list_append(xs, "el_map_get") let xs = native_list_append(xs, "fn(m: Map, k: String) -> Any") let xs = native_list_append(xs, "el_map_set") let xs = native_list_append(xs, "fn(m: Map, k: String, v: Any) -> Map") let xs = native_list_append(xs, "json_parse") let xs = native_list_append(xs, "fn(s: String) -> Any") let xs = native_list_append(xs, "json_stringify") let xs = native_list_append(xs, "fn(v: Any) -> String") let xs = native_list_append(xs, "json_get_string") let xs = native_list_append(xs, "fn(j: String, k: String) -> String") let xs = native_list_append(xs, "json_get_int") let xs = native_list_append(xs, "fn(j: String, k: String) -> Int") let xs = native_list_append(xs, "json_get_bool") let xs = native_list_append(xs, "fn(j: String, k: String) -> Bool") let xs = native_list_append(xs, "http_get") let xs = native_list_append(xs, "fn(url: String) -> String") let xs = native_list_append(xs, "http_post") let xs = native_list_append(xs, "fn(url: String, body: String) -> String") let xs = native_list_append(xs, "fs_read") let xs = native_list_append(xs, "fn(path: String) -> String") let xs = native_list_append(xs, "fs_write") let xs = native_list_append(xs, "fn(path: String, content: String) -> Bool") let xs = native_list_append(xs, "fs_list") let xs = native_list_append(xs, "fn(path: String) -> [String]") let xs = native_list_append(xs, "env") let xs = native_list_append(xs, "fn(key: String) -> String") let xs = native_list_append(xs, "uuid_new") let xs = native_list_append(xs, "fn() -> String") let xs = native_list_append(xs, "time_now") let xs = native_list_append(xs, "fn() -> Int") let xs = native_list_append(xs, "time_now_utc") let xs = native_list_append(xs, "fn() -> Int") let xs = native_list_append(xs, "sleep_ms") let xs = native_list_append(xs, "fn(ms: Int) -> Void") let xs = native_list_append(xs, "engram_node") let xs = native_list_append(xs, "fn(content: String, kind: String, salience: Float) -> String") let xs = native_list_append(xs, "engram_search") let xs = native_list_append(xs, "fn(q: String, limit: Int) -> [Map]") let xs = native_list_append(xs, "engram_activate") let xs = native_list_append(xs, "fn(q: String, depth: Int) -> [Map]") let xs = native_list_append(xs, "engram_neighbors") let xs = native_list_append(xs, "fn(id: String) -> [Map]") let xs = native_list_append(xs, "engram_connect") let xs = native_list_append(xs, "fn(from: String, to: String, w: Float, rel: String) -> Void") let xs = native_list_append(xs, "dharma_connect") let xs = native_list_append(xs, "fn(cgi: String) -> String") let xs = native_list_append(xs, "dharma_send") let xs = native_list_append(xs, "fn(channel: String, content: String) -> String") let xs = native_list_append(xs, "dharma_emit") let xs = native_list_append(xs, "fn(event: String, payload: String) -> Void") let xs = native_list_append(xs, "dharma_field") let xs = native_list_append(xs, "fn(event: String) -> Map") let xs = native_list_append(xs, "llm_call") let xs = native_list_append(xs, "fn(model: String, prompt: String) -> String") xs } fn keyword_doc(kw: String) -> String { if str_eq(kw, "let") { return "let name: Type = expr — declare an immutable binding." } if str_eq(kw, "fn") { return "fn name(params) -> Ret { body } — define a function." } if str_eq(kw, "type") { return "type Name { field: Type } — struct definition." } if str_eq(kw, "enum") { return "enum Name { Variant1, Variant2(Type) } — enum definition." } if str_eq(kw, "match") { return "match expr { Pattern => body } — pattern match." } if str_eq(kw, "return") { return "return value — exit the enclosing function." } if str_eq(kw, "if") { return "if cond { ... } else { ... } — conditional." } if str_eq(kw, "for") { return "for item in collection { ... } — iterate." } if str_eq(kw, "while") { return "while cond { ... } — loop while cond is truthy." } if str_eq(kw, "import") { return "import \"path.el\" — pull in another module." } if str_eq(kw, "from") { return "from module import { Name } — selective import." } if str_eq(kw, "vessel") { return "vessel \"name\" { ... } — manifest declaration." } if str_eq(kw, "cgi") { return "cgi \"name\" { dharma_id: ... } — CGI identity." } if str_eq(kw, "activate") { return "activate Type where \"query\" — spreading-activation retrieval." } if str_eq(kw, "sealed") { return "sealed { ... } — capability-restricted block." } if str_eq(kw, "protocol") { return "protocol Name { fn method(self) -> R } — trait-like." } if str_eq(kw, "impl") { return "impl Protocol for Type { ... } — protocol implementation." } if str_eq(kw, "with") { return "with retry/fallback clause." } if str_eq(kw, "retry") { return "retry times N — retry policy." } if str_eq(kw, "fallback") { return "fallback { default } — fallback on failure." } if str_eq(kw, "true") { return "Boolean literal." } if str_eq(kw, "false") { return "Boolean literal." } "" } // ── Source-driven type/fn discovery (no full parser) ───────────────────────── // // The Rust crate calls into el-types::TypeChecker for a real env. That // module isn't available in El yet, so we approximate by line-scan: find // every `type X {`, `enum X {`, `protocol X {`, and `fn X(` declaration. // Returns four parallel lists merged into one map for easy access. fn first_ident_in(rest: String) -> String { let trimmed: String = str_trim(rest) let n: Int = str_len(trimmed) if n == 0 { return "" } let chars: [String] = native_string_chars(trimmed) let i: Int = 0 let total: Int = native_list_len(chars) while i < total { let ch: String = native_list_get(chars, i) if !is_ident_ch(ch) { return str_slice(trimmed, 0, i) } let i = i + 1 } trimmed } // scan_decls — produce a Map with parallel lists: // "types" : [String] user-defined struct/enum/protocol names // "type_kind" : [String] "type" | "enum" | "protocol" matching above // "fns" : [String] user-defined function names // "type_lines" : [Int] 1-based line numbers // "fn_lines" : [Int] fn scan_decls(source: String) -> Map { let lines: [String] = str_split(source, "\n") let n: Int = native_list_len(lines) let types: [String] = native_list_empty() let kinds: [String] = native_list_empty() let fns: [String] = native_list_empty() let tlines: [Int] = native_list_empty() let flines: [Int] = native_list_empty() let i: Int = 0 while i < n { let line: String = native_list_get(lines, i) let trimmed: String = str_trim(line) let lineno: Int = i + 1 if str_starts_with(trimmed, "fn ") { let name: String = first_ident_in(str_slice(trimmed, 3, str_len(trimmed))) if !str_eq(name, "") { let fns = native_list_append(fns, name) let flines = native_list_append(flines, lineno) } } else { if str_starts_with(trimmed, "type ") { let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) if !str_eq(name, "") { let types = native_list_append(types, name) let kinds = native_list_append(kinds, "type") let tlines = native_list_append(tlines, lineno) } } else { if str_starts_with(trimmed, "enum ") { let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) if !str_eq(name, "") { let types = native_list_append(types, name) let kinds = native_list_append(kinds, "enum") let tlines = native_list_append(tlines, lineno) } } else { if str_starts_with(trimmed, "protocol ") { let name: String = first_ident_in(str_slice(trimmed, 9, str_len(trimmed))) if !str_eq(name, "") { let types = native_list_append(types, name) let kinds = native_list_append(kinds, "protocol") let tlines = native_list_append(tlines, lineno) } } } } } let i = i + 1 } { "types": types, "type_kind": kinds, "fns": fns, "type_lines": tlines, "fn_lines": flines } } // list_contains — case-sensitive membership test on [String]. fn list_contains_str(xs: [String], needle: String) -> Bool { let n: Int = native_list_len(xs) let i: Int = 0 while i < n { if str_eq(native_list_get(xs, i), needle) { return true } let i = i + 1 } false } // Score a label against a prefix: 0 if no prefix match, else (prefix_len/label_len). fn prefix_score(label: String, prefix: String) -> Float { if str_eq(prefix, "") { return 0.5 } let lower_label: String = str_to_lower(label) let lower_prefix: String = str_to_lower(prefix) if str_starts_with(lower_label, lower_prefix) { let lp: Int = str_len(lower_prefix) let ll: Int = str_len(lower_label) if ll == 0 { return 0.0 } return int_to_float(lp) / int_to_float(ll) } 0.0 } // Render a single completion item to JSON. fn completion_json(label: String, kind: String, detail: String, doc: String, score: Float) -> String { let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_str("label", label)) let parts = native_list_append(parts, json_kv_str("kind", kind)) let parts = native_list_append(parts, json_kv_str("detail", detail)) if str_eq(doc, "") { let parts = native_list_append(parts, json_kv_raw("documentation", "null")) } else { let parts = native_list_append(parts, json_kv_str("documentation", doc)) } let parts = native_list_append(parts, json_kv_float("score", score)) "{" + list_join(parts, ",") + "}" } // ── Public: completion ─────────────────────────────────────────────────────── fn lsp_complete(source: String, pos: Int) -> String { let prefix: String = extract_prefix(source, pos) let lower_prefix: String = str_to_lower(prefix) let prefix_empty: Bool = str_eq(prefix, "") let items: [String] = native_list_empty() // Keywords let kws: [String] = keywords_list() let nk: Int = native_list_len(kws) let i: Int = 0 while i < nk { let kw: String = native_list_get(kws, i) if prefix_empty { let s: Float = prefix_score(kw, prefix) let items = native_list_append(items, completion_json(kw, "keyword", "keyword", keyword_doc(kw), s)) } else { if str_starts_with(kw, prefix) { let s: Float = prefix_score(kw, prefix) let items = native_list_append(items, completion_json(kw, "keyword", "keyword", keyword_doc(kw), s)) } } let i = i + 1 } // Built-in types let bts: [String] = builtin_type_names() let nb: Int = native_list_len(bts) let i: Int = 0 while i < nb { let name: String = native_list_get(bts, i) let lname: String = str_to_lower(name) if prefix_empty { let s: Float = 0.5 + 0.1 let items = native_list_append(items, completion_json(name, "type", builtin_type_doc(name), "Built-in type: " + builtin_type_doc(name), s)) } else { if str_starts_with(lname, lower_prefix) { let s: Float = prefix_score(name, prefix) + 0.1 let items = native_list_append(items, completion_json(name, "type", builtin_type_doc(name), "Built-in type: " + builtin_type_doc(name), s)) } } let i = i + 1 } // User-defined types let decls: Map = scan_decls(source) let utypes: [String] = decls["types"] let ukinds: [String] = decls["type_kind"] let nu: Int = native_list_len(utypes) let i: Int = 0 while i < nu { let name: String = native_list_get(utypes, i) let kind: String = native_list_get(ukinds, i) let lname: String = str_to_lower(name) let include: Bool = false if prefix_empty { let include = true } if str_starts_with(lname, lower_prefix) { let include = true } if include { let s: Float = prefix_score(name, prefix) + 0.15 let items = native_list_append(items, completion_json(name, "type", kind + " " + name, "User-defined " + kind + " " + name, s)) } let i = i + 1 } // Built-in functions let bfns: [String] = builtin_fns() let nf: Int = native_list_len(bfns) let i: Int = 0 while i < nf { let name: String = native_list_get(bfns, i) let sig: String = native_list_get(bfns, i + 1) let lname: String = str_to_lower(name) let include: Bool = false if prefix_empty { let include = true } if str_starts_with(lname, lower_prefix) { let include = true } if include { let s: Float = prefix_score(name, prefix) + 0.11 let items = native_list_append(items, completion_json(name, "function", sig, "Built-in: " + name + " :: " + sig, s)) } let i = i + 2 } // User-defined functions let ufns: [String] = decls["fns"] let nuf: Int = native_list_len(ufns) let i: Int = 0 while i < nuf { let name: String = native_list_get(ufns, i) let lname: String = str_to_lower(name) let include: Bool = false if prefix_empty { let include = true } if str_starts_with(lname, lower_prefix) { let include = true } if include { let s: Float = prefix_score(name, prefix) + 0.12 let items = native_list_append(items, completion_json(name, "function", "fn " + name, "User function: " + name, s)) } let i = i + 1 } // Output is a list of pre-encoded JSON objects. Sorting by score is a // stretch goal — el's runtime has no sort-by-key primitive. Items are // already ordered by category (keywords first), which matches the most // common editor expectation. json_array_of(items) } // ── Public: hover ──────────────────────────────────────────────────────────── fn lsp_hover(source: String, pos: Int) -> String { let token: String = extract_token(source, pos) if str_eq(token, "") { return "null" } // Built-in primitives let doc: String = builtin_type_doc(token) if !str_eq(doc, "") { let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_str("type_name", token)) let parts = native_list_append(parts, json_kv_str("documentation", doc)) let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) return "{" + list_join(parts, ",") + "}" } // User-declared types let decls: Map = scan_decls(source) let utypes: [String] = decls["types"] let ukinds: [String] = decls["type_kind"] let nu: Int = native_list_len(utypes) let i: Int = 0 while i < nu { let name: String = native_list_get(utypes, i) if str_eq(name, token) { let kind: String = native_list_get(ukinds, i) let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_str("type_name", token)) let parts = native_list_append(parts, json_kv_str("documentation", kind + " " + token)) let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) return "{" + list_join(parts, ",") + "}" } let i = i + 1 } // User-declared functions let ufns: [String] = decls["fns"] let nuf: Int = native_list_len(ufns) let i: Int = 0 while i < nuf { let name: String = native_list_get(ufns, i) if str_eq(name, token) { let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_str("type_name", token)) let parts = native_list_append(parts, json_kv_str("documentation", "fn " + token)) let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) return "{" + list_join(parts, ",") + "}" } let i = i + 1 } // Built-in functions let bfns: [String] = builtin_fns() let nf: Int = native_list_len(bfns) let i: Int = 0 while i < nf { let name: String = native_list_get(bfns, i) if str_eq(name, token) { let sig: String = native_list_get(bfns, i + 1) let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_str("type_name", token)) let parts = native_list_append(parts, json_kv_str("documentation", "fn " + token + " :: " + sig)) let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) return "{" + list_join(parts, ",") + "}" } let i = i + 2 } // Keyword let kdoc: String = keyword_doc(token) if !str_eq(kdoc, "") { let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_str("type_name", token)) let parts = native_list_append(parts, json_kv_str("documentation", kdoc)) let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) return "{" + list_join(parts, ",") + "}" } "null" } // ── Public: diagnostics ────────────────────────────────────────────────────── // // Without the type checker port we surface only structural diagnostics: // * unterminated string literal // * unbalanced braces / parens / brackets // * unknown type referenced after `:` in a let binding (unknown to source + // builtins) — best-effort, conservative. fn diag_json(message: String, severity: String, line: Int, col: Int) -> String { let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_str("message", message)) let parts = native_list_append(parts, json_kv_str("severity", severity)) if line > 0 { let parts = native_list_append(parts, json_kv_int("line", line)) let parts = native_list_append(parts, json_kv_int("col", col)) } else { let parts = native_list_append(parts, json_kv_raw("line", "null")) let parts = native_list_append(parts, json_kv_raw("col", "null")) } "{" + list_join(parts, ",") + "}" } fn lsp_diagnostics(source: String) -> String { let chars: [String] = native_string_chars(source) let n: Int = native_list_len(chars) let diags: [String] = native_list_empty() // Bracket balance + unterminated string scan let depth_brace: Int = 0 let depth_paren: Int = 0 let depth_brack: Int = 0 let i: Int = 0 let line: Int = 1 let col: Int = 1 let in_string: Bool = false let string_start_line: Int = 0 let string_start_col: Int = 0 let in_comment: Bool = false while i < n { let ch: String = native_list_get(chars, i) if in_comment { if ch == "\n" { let in_comment = false let line = line + 1 let col = 1 } else { let col = col + 1 } let i = i + 1 } else { if in_string { if ch == "\\" { let i = i + 1 let col = col + 1 if i < n { let esc: String = native_list_get(chars, i) if esc == "\n" { let line = line + 1 let col = 1 } else { let col = col + 1 } let i = i + 1 } } else { if ch == "\"" { let in_string = false let i = i + 1 let col = col + 1 } else { if ch == "\n" { let line = line + 1 let col = 1 let i = i + 1 } else { let col = col + 1 let i = i + 1 } } } } else { if ch == "\"" { let in_string = true let string_start_line = line let string_start_col = col let i = i + 1 let col = col + 1 } else { if ch == "/" { let next_i: Int = i + 1 let is_line_comment: Bool = false if next_i < n { let next_ch: String = native_list_get(chars, next_i) if next_ch == "/" { let is_line_comment = true } } if is_line_comment { let in_comment = true let i = i + 2 let col = col + 2 } else { let i = i + 1 let col = col + 1 } } else { if ch == "{" { let depth_brace = depth_brace + 1 let i = i + 1 let col = col + 1 } else { if ch == "}" { let depth_brace = depth_brace - 1 if depth_brace < 0 { let diags = native_list_append(diags, diag_json("unmatched `}`", "error", line, col)) let depth_brace = 0 } let i = i + 1 let col = col + 1 } else { if ch == "(" { let depth_paren = depth_paren + 1 let i = i + 1 let col = col + 1 } else { if ch == ")" { let depth_paren = depth_paren - 1 if depth_paren < 0 { let diags = native_list_append(diags, diag_json("unmatched `)`", "error", line, col)) let depth_paren = 0 } let i = i + 1 let col = col + 1 } else { if ch == "[" { let depth_brack = depth_brack + 1 let i = i + 1 let col = col + 1 } else { if ch == "]" { let depth_brack = depth_brack - 1 if depth_brack < 0 { let diags = native_list_append(diags, diag_json("unmatched `]`", "error", line, col)) let depth_brack = 0 } let i = i + 1 let col = col + 1 } else { if ch == "\n" { let line = line + 1 let col = 1 let i = i + 1 } else { let i = i + 1 let col = col + 1 } } } } } } } } } } } } if in_string { let diags = native_list_append(diags, diag_json("unterminated string literal", "error", string_start_line, string_start_col)) } if depth_brace > 0 { let diags = native_list_append(diags, diag_json("unclosed `{` (missing `}`)", "error", 0, 0)) } if depth_paren > 0 { let diags = native_list_append(diags, diag_json("unclosed `(` (missing `)`)", "error", 0, 0)) } if depth_brack > 0 { let diags = native_list_append(diags, diag_json("unclosed `[` (missing `]`)", "error", 0, 0)) } json_array_of(diags) } // ── Public: outline ────────────────────────────────────────────────────────── fn outline_item_json(kind: String, name: String, line: Int) -> String { let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_str("kind", kind)) let parts = native_list_append(parts, json_kv_str("name", name)) let parts = native_list_append(parts, json_kv_int("line", line)) "{" + list_join(parts, ",") + "}" } fn lsp_outline(source: String) -> String { let lines: [String] = str_split(source, "\n") let n: Int = native_list_len(lines) let items: [String] = native_list_empty() let i: Int = 0 while i < n { let line: String = native_list_get(lines, i) let trimmed: String = str_trim(line) let lineno: Int = i + 1 if str_starts_with(trimmed, "fn ") { let name: String = first_ident_in(str_slice(trimmed, 3, str_len(trimmed))) if !str_eq(name, "") { let items = native_list_append(items, outline_item_json("fn", name, lineno)) } } else { if str_starts_with(trimmed, "type ") { let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) if !str_eq(name, "") { let items = native_list_append(items, outline_item_json("type", name, lineno)) } } else { if str_starts_with(trimmed, "enum ") { let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) if !str_eq(name, "") { let items = native_list_append(items, outline_item_json("enum", name, lineno)) } } else { if str_starts_with(trimmed, "protocol ") { let name: String = first_ident_in(str_slice(trimmed, 9, str_len(trimmed))) if !str_eq(name, "") { let items = native_list_append(items, outline_item_json("protocol", name, lineno)) } } else { if str_starts_with(trimmed, "impl ") { let rest: String = str_trim(str_slice(trimmed, 5, str_len(trimmed))) let brace: Int = str_index_of(rest, "{") let name: String = rest if brace > 0 { let name = str_trim(str_slice(rest, 0, brace)) } if !str_eq(name, "") { let items = native_list_append(items, outline_item_json("impl", name, lineno)) } } } } } } let i = i + 1 } json_array_of(items) } // ── Public: format ─────────────────────────────────────────────────────────── // // Idiomatic-El formatter. Until `el fmt` is a real thing, we apply a minimal // canonicalisation pass: // * trim trailing whitespace on every line // * collapse 3+ blank lines to 2 // * ensure file ends with exactly one newline // This is safe for round-tripping and a noticeable quality-of-life win in // the editor. fn format_strip_trailing(line: String) -> String { // str_trim trims both ends; we want only trailing whitespace stripped. let n: Int = str_len(line) if n == 0 { return line } let chars: [String] = native_string_chars(line) let i: Int = n - 1 let trimming: Bool = true while trimming { if i < 0 { let trimming = false } else { let ch: String = native_list_get(chars, i) if ch == " " { let i = i - 1 } else { if ch == "\t" { let i = i - 1 } else { let trimming = false } } } } str_slice(line, 0, i + 1) } fn lsp_format(source: String) -> String { let lines: [String] = str_split(source, "\n") let n: Int = native_list_len(lines) let cleaned: [String] = native_list_empty() let i: Int = 0 let blank_run: Int = 0 while i < n { let line: String = native_list_get(lines, i) let stripped: String = format_strip_trailing(line) if str_eq(stripped, "") { let blank_run = blank_run + 1 if blank_run <= 2 { let cleaned = native_list_append(cleaned, "") } } else { let blank_run = 0 let cleaned = native_list_append(cleaned, stripped) } let i = i + 1 } let joined: String = list_join(cleaned, "\n") // Ensure trailing newline. let final: String = joined if !str_ends_with(final, "\n") { let final = final + "\n" } let changed: String = "false" if !str_eq(final, source) { let changed = "true" } "{" + json_kv_str("content", final) + "," + json_kv_raw("changed", changed) + "}" } // ── Public: type graph ─────────────────────────────────────────────────────── fn lsp_type_graph(source: String) -> String { let nodes: [String] = native_list_empty() let edges: [String] = native_list_empty() // Built-in nodes let bts: [String] = builtin_type_names() let nb: Int = native_list_len(bts) let i: Int = 0 while i < nb { let name: String = native_list_get(bts, i) let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_str("id", name)) let parts = native_list_append(parts, json_kv_str("name", name)) let parts = native_list_append(parts, json_kv_str("kind", "builtin")) let parts = native_list_append(parts, json_kv_raw("fields", "[]")) let nodes = native_list_append(nodes, "{" + list_join(parts, ",") + "}") let i = i + 1 } // User-declared types let decls: Map = scan_decls(source) let utypes: [String] = decls["types"] let ukinds: [String] = decls["type_kind"] let tlines: [Int] = decls["type_lines"] let nu: Int = native_list_len(utypes) let i: Int = 0 let bts_dup: [String] = builtin_type_names() while i < nu { let name: String = native_list_get(utypes, i) let kind: String = native_list_get(ukinds, i) // Skip if already a builtin (paranoia) if !list_contains_str(bts_dup, name) { let fields: [String] = collect_struct_fields(source, name) let field_jsons: [String] = native_list_empty() let fn2: Int = native_list_len(fields) let j: Int = 0 while j < fn2 { let field_jsons = native_list_append(field_jsons, json_str(native_list_get(fields, j))) let j = j + 1 } let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_str("id", name)) let parts = native_list_append(parts, json_kv_str("name", name)) let parts = native_list_append(parts, json_kv_str("kind", kind)) let parts = native_list_append(parts, json_kv_raw("fields", json_array_of(field_jsons))) let nodes = native_list_append(nodes, "{" + list_join(parts, ",") + "}") // Edges: scan each field for a known type reference. let j: Int = 0 while j < fn2 { let field_str: String = native_list_get(fields, j) let target: String = referenced_type(field_str, utypes) if !str_eq(target, "") { let eparts: [String] = native_list_empty() let eparts = native_list_append(eparts, json_kv_str("from", name)) let eparts = native_list_append(eparts, json_kv_str("to", target)) let eparts = native_list_append(eparts, json_kv_str("label", "field")) let edges = native_list_append(edges, "{" + list_join(eparts, ",") + "}") } let j = j + 1 } } let i = i + 1 } "{" + json_kv_raw("nodes", json_array_of(nodes)) + "," + json_kv_raw("edges", json_array_of(edges)) + "}" } // Extract the field list of a `type Name { ... }` block as ["field: Type", ...] // or for an enum, the variant labels. Best-effort line-by-line scan. fn collect_struct_fields(source: String, type_name: String) -> [String] { let lines: [String] = str_split(source, "\n") let n: Int = native_list_len(lines) let fields: [String] = native_list_empty() let inside: Bool = false let i: Int = 0 let header_a: String = "type " + type_name let header_b: String = "enum " + type_name let header_c: String = "protocol " + type_name while i < n { let line: String = native_list_get(lines, i) let trimmed: String = str_trim(line) if !inside { if str_starts_with(trimmed, header_a) { let inside = true } if str_starts_with(trimmed, header_b) { let inside = true } if str_starts_with(trimmed, header_c) { let inside = true } } else { if str_starts_with(trimmed, "}") { return fields } // skip the opening "{" line if !str_eq(trimmed, "{") { if !str_eq(trimmed, "") { if !str_starts_with(trimmed, "//") { // strip a trailing comma if present let entry: String = trimmed if str_ends_with(entry, ",") { let entry = str_slice(entry, 0, str_len(entry) - 1) } let fields = native_list_append(fields, str_trim(entry)) } } } } let i = i + 1 } fields } // Pick the first known type name out of a `field: Type` string. Stops at non-ident. fn referenced_type(field_str: String, known_types: [String]) -> String { // Find ":" — type follows. let colon: Int = str_index_of(field_str, ":") if colon < 0 { return "" } let after: String = str_trim(str_slice(field_str, colon + 1, str_len(field_str))) // Strip array brackets and optional marker. let trimmed_l: String = after if str_starts_with(trimmed_l, "[") { let end: Int = str_index_of(trimmed_l, "]") if end > 1 { let trimmed_l = str_trim(str_slice(trimmed_l, 1, end)) } } // First identifier. let ident: String = first_ident_in(trimmed_l) if str_eq(ident, "") { return "" } let bts: [String] = builtin_type_names() if list_contains_str(bts, ident) { return ident } if list_contains_str(known_types, ident) { return ident } "" } // ── Public: definition / references ────────────────────────────────────────── fn lsp_definition(source: String, pos: Int) -> String { let word: String = extract_token(source, pos) if str_eq(word, "") { return "null" } let patterns: [String] = native_list_empty() let patterns = native_list_append(patterns, "fn " + word) let patterns = native_list_append(patterns, "type " + word) let patterns = native_list_append(patterns, "enum " + word) let patterns = native_list_append(patterns, "protocol " + word) let np: Int = native_list_len(patterns) let lines: [String] = str_split(source, "\n") let nl: Int = native_list_len(lines) let i: Int = 0 while i < nl { let line: String = native_list_get(lines, i) let j: Int = 0 while j < np { let pat: String = native_list_get(patterns, j) let idx: Int = str_index_of(line, pat) if idx >= 0 { let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_int("line", i + 1)) let parts = native_list_append(parts, json_kv_int("col", idx + 1)) let parts = native_list_append(parts, json_kv_str("snippet", str_trim(line))) return "{" + list_join(parts, ",") + "}" } let j = j + 1 } let i = i + 1 } "null" } fn lsp_references(source: String, pos: Int) -> String { let word: String = extract_token(source, pos) if str_eq(word, "") { return "[]" } let lines: [String] = str_split(source, "\n") let nl: Int = native_list_len(lines) let refs: [String] = native_list_empty() let i: Int = 0 while i < nl { let line: String = native_list_get(lines, i) let line_chars: [String] = native_string_chars(line) let llen: Int = native_list_len(line_chars) let wlen: Int = str_len(word) let off: Int = 0 let scanning: Bool = true while scanning { let idx: Int = str_index_of(str_slice(line, off, str_len(line)), word) if idx < 0 { let scanning = false } else { let abs: Int = off + idx // Word-boundary check let before_ok: Bool = true if abs > 0 { let prev: String = native_list_get(line_chars, abs - 1) if is_ident_ch(prev) { let before_ok = false } } let after_ok: Bool = true let after_pos: Int = abs + wlen if after_pos < llen { let nxt: String = native_list_get(line_chars, after_pos) if is_ident_ch(nxt) { let after_ok = false } } if before_ok { if after_ok { let parts: [String] = native_list_empty() let parts = native_list_append(parts, json_kv_int("line", i + 1)) let parts = native_list_append(parts, json_kv_int("col", abs + 1)) let parts = native_list_append(parts, json_kv_str("snippet", str_trim(line))) let refs = native_list_append(refs, "{" + list_join(parts, ",") + "}") } } let off = abs + wlen if off >= str_len(line) { let scanning = false } } } let i = i + 1 } json_array_of(refs) } // ── JSON-RPC dispatch ──────────────────────────────────────────────────────── // // Method names follow LSP-ish conventions but stay aligned with the Rust crate's // HTTP surface. Params are a JSON string the dispatcher pulls fields out of via // json_get_string / json_get_int. // // "textDocument/completion" → params { source, pos } // "textDocument/hover" → params { source, pos } // "textDocument/diagnostic" → params { source } // "textDocument/formatting" → params { source } // "textDocument/documentSymbol"→ params { source } // "textDocument/definition" → params { source, pos } // "textDocument/references" → params { source, pos } // "el/typeGraph" → params { source } fn lsp_jsonrpc(method: String, params_json: String) -> String { let source: String = json_get_string(params_json, "source") let pos: Int = json_get_int(params_json, "pos") if str_eq(method, "textDocument/completion") { return lsp_complete(source, pos) } if str_eq(method, "textDocument/hover") { return lsp_hover(source, pos) } if str_eq(method, "textDocument/diagnostic") { return lsp_diagnostics(source) } if str_eq(method, "textDocument/formatting") { return lsp_format(source) } if str_eq(method, "textDocument/documentSymbol") { return lsp_outline(source) } if str_eq(method, "textDocument/definition") { return lsp_definition(source, pos) } if str_eq(method, "textDocument/references") { return lsp_references(source, pos) } if str_eq(method, "el/typeGraph") { return lsp_type_graph(source) } "{\"error\":\"unknown method: " + json_escape(method) + "\"}" } // ── stdio loop (best-effort; see runtime gap note in the report) ───────────── // // LSP framing is `Content-Length: N\r\n\r\n` then exactly N bytes. The El // runtime exposes only line-buffered `readline()`, which strips `\n` and is // hostile to the binary body read. We approximate by reading lines until a // blank line ends the headers, then reading exactly one more line as the // body (works for compact, no-newline payloads — i.e. minified JSON-RPC). // A dedicated `read_n(bytes)` runtime primitive would unblock the strict // implementation. See report. fn read_headers() -> Map { let content_len: Int = 0 let scanning: Bool = true while scanning { let line: String = readline() if str_eq(line, "") { let scanning = false } else { let cl_marker: String = "Content-Length:" if str_starts_with(line, cl_marker) { let val: String = str_trim(str_slice(line, str_len(cl_marker), str_len(line))) let content_len = str_to_int(val) } } } { "content_length": content_len } } fn lsp_stdio_loop() -> Void { let running: Bool = true while running { let headers: Map = read_headers() let body: String = readline() if str_eq(body, "") { let running = false } else { let method: String = json_get_string(body, "method") let id_val: String = json_get_raw(body, "id") if str_eq(id_val, "") { let id_val = "null" } let params_raw: String = json_get_raw(body, "params") let result: String = lsp_jsonrpc(method, params_raw) let resp: String = "{\"jsonrpc\":\"2.0\",\"id\":" + id_val + ",\"result\":" + result + "}" let frame: String = "Content-Length: " + int_to_str(str_len(resp)) + "\r\n\r\n" + resp print(frame) } } } // ── Entry ──────────────────────────────────────────────────────────────────── // // When invoked as a standalone binary, default to stdio JSON-RPC so editors // that drive the language server directly (without the HTTP wrapper) work // out of the box. Pass a single CLI arg to short-circuit: // ./el-lsp probe → emits a self-test diagnostic + exits // ./el-lsp version → emits version + exits fn lsp_main() -> Void { let argv: [String] = args() let n: Int = native_list_len(argv) if n > 0 { let cmd: String = native_list_get(argv, 0) if str_eq(cmd, "version") { println("el-lsp 0.1.0") return } if str_eq(cmd, "probe") { let sample: String = "fn main() -> Void { let x: Int = 42 }" println(lsp_diagnostics(sample)) return } } lsp_stdio_loop() } // main.el — el-plugin-host // // Plugin lifecycle manager for el-ide. Pure-El port of the el-plugin-host // Rust crate. // // Plugins are stored in process-global state via state_get / state_set. // Each plugin lives under a key prefix `plugin//`. The // canonical list of plugin names is maintained at `plugins/index` as a // comma-separated string (we don't have list serialisation in state yet). // // Public API: // plugin_init() — seed first-party plugins (idempotent) // plugin_list_json() -> String — JSON array of all plugins // plugin_get_json(name) -> String — JSON object or "null" // plugin_install(name) -> String — JSON { ok | error } // plugin_remove(name) -> String // plugin_enable(name) -> String // plugin_disable(name) -> String // ── State helpers (string K/V) ─────────────────────────────────────────────── fn pkey(name: String, field: String) -> String { "plugin/" + name + "/" + field } fn put_str(name: String, field: String, value: String) -> Void { state_set(pkey(name, field), value) } fn put_bool(name: String, field: String, value: Bool) -> Void { if value { state_set(pkey(name, field), "true") } if !value { state_set(pkey(name, field), "false") } } fn get_str(name: String, field: String) -> String { state_get(pkey(name, field)) } fn get_bool(name: String, field: String) -> Bool { str_eq(state_get(pkey(name, field)), "true") } // Plugin index: comma-separated list of names at "plugins/index". fn index_list() -> [String] { let raw: String = state_get("plugins/index") if str_eq(raw, "") { return native_list_empty() } str_split(raw, ",") } fn index_set(names: [String]) -> Void { state_set("plugins/index", list_join(names, ",")) } fn index_contains(name: String) -> Bool { let names: [String] = index_list() let n: Int = native_list_len(names) let i: Int = 0 while i < n { if str_eq(native_list_get(names, i), name) { return true } let i = i + 1 } false } fn index_add(name: String) -> Void { if !index_contains(name) { let names: [String] = index_list() let names = native_list_append(names, name) index_set(names) } } // ── Initialisation ─────────────────────────────────────────────────────────── fn register_plugin(name: String, version: String, description: String, installed: Bool, enabled: Bool, hooks: String, first_party: Bool) -> Void { put_str(name, "name", name) put_str(name, "version", version) put_str(name, "description", description) put_bool(name, "installed", installed) put_bool(name, "enabled", enabled) put_str(name, "hooks", hooks) put_bool(name, "first_party", first_party) index_add(name) } fn plugin_init() -> Void { if str_eq(state_get("plugins/initialised"), "true") { return } register_plugin("el-theme-dark", "1.0.0", "Dark theme — the default Engram IDE theme.", true, true, "", true) register_plugin("el-theme-light", "1.0.0", "Light theme for high-ambient-light environments.", false, false, "", true) register_plugin("el-fmt", "0.1.0", "Code formatter — auto-formats .el files on save.", true, true, "on_save", true) register_plugin("el-doc", "0.1.0", "Documentation generator — produces HTML docs from type definitions.", false, false, "custom_tool", true) register_plugin("el-test", "0.1.0", "Test runner with inline results displayed in the editor gutter.", false, false, "on_build_complete,custom_panel", true) state_set("plugins/initialised", "true") } // ── Serialisation ──────────────────────────────────────────────────────────── fn json_escape_str(s: String) -> String { let chars: [String] = native_string_chars(s) let n: Int = native_list_len(chars) let out: String = "" let i: Int = 0 while i < n { let ch: String = native_list_get(chars, i) if ch == "\"" { let out = out + "\\\"" } else { if ch == "\\" { let out = out + "\\\\" } else { if ch == "\n" { let out = out + "\\n" } else { let out = out + ch } } } let i = i + 1 } out } fn quote_json(s: String) -> String { "\"" + json_escape_str(s) + "\"" } fn hooks_to_json(hooks_csv: String) -> String { if str_eq(hooks_csv, "") { return "[]" } let xs: [String] = str_split(hooks_csv, ",") let n: Int = native_list_len(xs) let out: String = "[" let i: Int = 0 while i < n { if i > 0 { let out = out + "," } let out = out + quote_json(native_list_get(xs, i)) let i = i + 1 } out + "]" } fn plugin_to_json(name: String) -> String { let parts: [String] = native_list_empty() let parts = native_list_append(parts, "\"name\":" + quote_json(get_str(name, "name"))) let parts = native_list_append(parts, "\"version\":" + quote_json(get_str(name, "version"))) let parts = native_list_append(parts, "\"description\":" + quote_json(get_str(name, "description"))) let installed: String = get_str(name, "installed") let enabled: String = get_str(name, "enabled") let first_party: String = get_str(name, "first_party") let parts = native_list_append(parts, "\"installed\":" + installed) let parts = native_list_append(parts, "\"enabled\":" + enabled) let parts = native_list_append(parts, "\"hooks\":" + hooks_to_json(get_str(name, "hooks"))) let parts = native_list_append(parts, "\"first_party\":" + first_party) "{" + list_join(parts, ",") + "}" } fn plugin_list_json() -> String { plugin_init() let names: [String] = index_list() let n: Int = native_list_len(names) let out: String = "[" let i: Int = 0 while i < n { if i > 0 { let out = out + "," } let out = out + plugin_to_json(native_list_get(names, i)) let i = i + 1 } out + "]" } fn plugin_get_json(name: String) -> String { plugin_init() if !index_contains(name) { return "null" } plugin_to_json(name) } // ── Mutations ──────────────────────────────────────────────────────────────── fn plugin_install(name: String) -> String { plugin_init() if !index_contains(name) { return "{\"error\":\"plugin registry is not available (registry URL not configured)\"}" } if get_bool(name, "installed") { return "{\"error\":\"plugin '" + json_escape_str(name) + "' is already installed\"}" } put_bool(name, "installed", true) put_bool(name, "enabled", true) plugin_to_json(name) } fn plugin_remove(name: String) -> String { plugin_init() if !index_contains(name) { return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}" } if str_eq(name, "el-theme-dark") { return "{\"error\":\"plugin '" + json_escape_str(name) + "' cannot be removed: it is a required built-in\"}" } put_bool(name, "installed", false) put_bool(name, "enabled", false) "{\"ok\":true}" } fn plugin_enable(name: String) -> String { plugin_init() if !index_contains(name) { return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}" } put_bool(name, "enabled", true) "{\"ok\":true}" } fn plugin_disable(name: String) -> String { plugin_init() if !index_contains(name) { return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}" } put_bool(name, "enabled", false) "{\"ok\":true}" } // ── Standalone CLI ─────────────────────────────────────────────────────────── fn host_main() -> Void { let argv: [String] = args() let n: Int = native_list_len(argv) if n == 0 { println(plugin_list_json()) return } let cmd: String = native_list_get(argv, 0) if str_eq(cmd, "list") { println(plugin_list_json()) return } if str_eq(cmd, "get") { if n >= 2 { println(plugin_get_json(native_list_get(argv, 1))) } return } if str_eq(cmd, "install") { if n >= 2 { println(plugin_install(native_list_get(argv, 1))) } return } if str_eq(cmd, "remove") { if n >= 2 { println(plugin_remove(native_list_get(argv, 1))) } return } if str_eq(cmd, "enable") { if n >= 2 { println(plugin_enable(native_list_get(argv, 1))) } return } if str_eq(cmd, "disable") { if n >= 2 { println(plugin_disable(native_list_get(argv, 1))) } return } println("usage: el-plugin-host [list|get|install|remove|enable|disable] ") } // main.el — el-ide-server // // HTTP backend for the Engram IDE. Pure-El port of the el-ide-server Rust // crate. Wires the el-lsp and el-plugin-host vessels together and serves // the IDE-frontend protocol used by the React-style frontend. // // El compilation note: `import` statements are parsed but the codegen // concatenates nothing. The build script for this vessel concatenates // // vessels/el-lsp/src/main.el (without the trailing entry call) // vessels/el-plugin-host/src/main.el // vessels/el-ide-server/src/main.el (this file) // // into a single _combined.el before invoking elc. This file therefore // assumes the lsp_* and plugin_* functions defined in those siblings are // in scope. See README / build.sh. // // All routes serve JSON over HTTP. Query parameters are parsed by hand // (the runtime gives us strings and a request body). Streaming endpoints // (SSE) are emulated by returning the full output once — the runtime's // http_serve handler signature is response-as-string, so true server-sent // streaming requires a runtime extension. Flagged in the report. // ── Tiny utility layer ─────────────────────────────────────────────────────── fn jstr(s: String) -> String { let chars: [String] = native_string_chars(s) let n: Int = native_list_len(chars) let out: String = "\"" let i: Int = 0 while i < n { let ch: String = native_list_get(chars, i) if ch == "\"" { let out = out + "\\\"" } else { if ch == "\\" { let out = out + "\\\\" } else { if ch == "\n" { let out = out + "\\n" } else { if ch == "\r" { let out = out + "\\r" } else { if ch == "\t" { let out = out + "\\t" } else { let out = out + ch } } } } } let i = i + 1 } out + "\"" } fn jerr(msg: String) -> String { "{\"error\":" + jstr(msg) + "}" } fn ok_obj() -> String { "{\"ok\":true}" } // ── URL/query parsing ──────────────────────────────────────────────────────── fn url_decode(s: String) -> String { let chars: [String] = native_string_chars(s) let n: Int = native_list_len(chars) let out: String = "" let i: Int = 0 while i < n { let ch: String = native_list_get(chars, i) if ch == "+" { let out = out + " " let i = i + 1 } else { if ch == "%" { // Read two hex digits if i + 2 < n { let hex: String = native_list_get(chars, i + 1) + native_list_get(chars, i + 2) let code: Int = hex_to_int(hex) let out = out + char_from_code(code) let i = i + 3 } else { let out = out + ch let i = i + 1 } } else { let out = out + ch let i = i + 1 } } } out } fn hex_digit(ch: String) -> Int { if ch == "0" { return 0 } if ch == "1" { return 1 } if ch == "2" { return 2 } if ch == "3" { return 3 } if ch == "4" { return 4 } if ch == "5" { return 5 } if ch == "6" { return 6 } if ch == "7" { return 7 } if ch == "8" { return 8 } if ch == "9" { return 9 } if ch == "a" { return 10 } if ch == "A" { return 10 } if ch == "b" { return 11 } if ch == "B" { return 11 } if ch == "c" { return 12 } if ch == "C" { return 12 } if ch == "d" { return 13 } if ch == "D" { return 13 } if ch == "e" { return 14 } if ch == "E" { return 14 } if ch == "f" { return 15 } if ch == "F" { return 15 } -1 } fn hex_to_int(hex: String) -> Int { let chars: [String] = native_string_chars(hex) let n: Int = native_list_len(chars) let acc: Int = 0 let i: Int = 0 while i < n { let d: Int = hex_digit(native_list_get(chars, i)) if d < 0 { return acc } let acc = acc * 16 + d let i = i + 1 } acc } // Map an ASCII code point back to its single-byte string. Above 127 the // runtime would need char_from_code(>127) → multi-byte; we only need // printable ASCII for query strings here. fn char_from_code(code: Int) -> String { let table: String = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" if code < 32 { return " " } if code > 126 { return "?" } str_slice(table, code - 32, code - 31) } fn split_path_query(path: String) -> Map { let q: Int = str_index_of(path, "?") if q < 0 { return { "path": path, "query": "" } } { "path": str_slice(path, 0, q), "query": str_slice(path, q + 1, str_len(path)) } } // query_get: pull the value of a single key out of `a=1&b=hello%20world`. fn query_get(query: String, key: String) -> String { if str_eq(query, "") { return "" } let pairs: [String] = str_split(query, "&") let n: Int = native_list_len(pairs) let prefix: String = key + "=" let i: Int = 0 while i < n { let pair: String = native_list_get(pairs, i) if str_starts_with(pair, prefix) { return url_decode(str_slice(pair, str_len(prefix), str_len(pair))) } if str_eq(pair, key) { return "" } let i = i + 1 } "" } // ── Configuration ──────────────────────────────────────────────────────────── fn cfg_port() -> Int { let p: String = env("EL_IDE_PORT") if str_eq(p, "") { return 7771 } str_to_int(p) } fn cfg_project_path() -> String { let p: String = env("EL_IDE_PROJECT_PATH") if str_eq(p, "") { return "." } p } fn cfg_engram_url() -> String { let u: String = env("EL_ENGRAM_URL") if str_eq(u, "") { return "http://localhost:8742" } u } fn cfg_el_binary() -> String { let b: String = env("EL_BINARY") if str_eq(b, "") { return "el" } b } // ── Path safety ────────────────────────────────────────────────────────────── fn safe_join(rel: String) -> String { let root: String = cfg_project_path() // Reject obvious traversals. if str_contains(rel, "..") { return "" } if str_starts_with(rel, "/") { return "" } if str_eq(rel, ".") { return root } root + "/" + rel } // ── Status route ───────────────────────────────────────────────────────────── fn route_status() -> String { let parts: [String] = native_list_empty() let parts = native_list_append(parts, "\"project_name\":" + jstr(cfg_project_path())) let parts = native_list_append(parts, "\"project_path\":" + jstr(cfg_project_path())) let parts = native_list_append(parts, "\"version\":" + jstr("0.1.0")) let parts = native_list_append(parts, "\"engine\":" + jstr("el-ide")) "{" + list_join(parts, ",") + "}" } // ── Config route ───────────────────────────────────────────────────────────── fn route_config() -> String { let parts: [String] = native_list_empty() let parts = native_list_append(parts, "\"port\":" + int_to_str(cfg_port())) let parts = native_list_append(parts, "\"project_path\":" + jstr(cfg_project_path())) let parts = native_list_append(parts, "\"engram_url\":" + jstr(cfg_engram_url())) let parts = native_list_append(parts, "\"el_binary\":" + jstr(cfg_el_binary())) "{" + list_join(parts, ",") + "}" } // ── File API ───────────────────────────────────────────────────────────────── fn route_files_list(query: String) -> String { let rel: String = query_get(query, "path") if str_eq(rel, "") { let rel = "." } let target: String = safe_join(rel) if str_eq(target, "") { return jerr("path escapes project root") } let entries: [String] = fs_list(target) let n: Int = native_list_len(entries) let out: String = "[" let i: Int = 0 while i < n { let name: String = native_list_get(entries, i) // Skip hidden + noise dirs let skip: Bool = false if str_starts_with(name, ".") { let skip = true } if str_eq(name, "target") { let skip = true } if str_eq(name, "node_modules") { let skip = true } if str_eq(name, "dist") { let skip = true } if !skip { if i > 0 { if !str_eq(out, "[") { let out = out + "," } } // Approximate is_dir by checking for an extension let dot: Int = str_index_of(name, ".") let is_dir: String = "false" if dot < 0 { let is_dir = "true" } let out = out + "{\"name\":" + jstr(name) + ",\"path\":" + jstr(rel + "/" + name) + ",\"is_dir\":" + is_dir + ",\"children\":null}" } let i = i + 1 } out + "]" } fn route_file_read(query: String) -> String { let rel: String = query_get(query, "path") if str_eq(rel, "") { return jerr("missing path parameter") } let target: String = safe_join(rel) if str_eq(target, "") { return jerr("path escapes project root") } let content: String = fs_read(target) "{\"path\":" + jstr(rel) + ",\"content\":" + jstr(content) + "}" } fn route_file_write(body: String) -> String { let rel: String = json_get_string(body, "path") let content: String = json_get_string(body, "content") if str_eq(rel, "") { return jerr("missing path") } let target: String = safe_join(rel) if str_eq(target, "") { return jerr("path escapes project root") } let ok: Bool = fs_write(target, content) if ok { return ok_obj() } jerr("cannot write file") } // ── LSP routes (delegate to inlined lsp_* functions) ───────────────────────── fn route_lsp_complete(query: String) -> String { let source: String = query_get(query, "source") let pos: Int = str_to_int(query_get(query, "pos")) lsp_complete(source, pos) } fn route_lsp_hover(query: String) -> String { let source: String = query_get(query, "source") let pos: Int = str_to_int(query_get(query, "pos")) lsp_hover(source, pos) } fn route_lsp_errors(query: String) -> String { let source: String = query_get(query, "source") lsp_diagnostics(source) } fn route_lsp_jsonrpc(body: String) -> String { let method: String = json_get_string(body, "method") let params_raw: String = json_get_raw(body, "params") let id_val: String = json_get_raw(body, "id") if str_eq(id_val, "") { let id_val = "null" } let result: String = lsp_jsonrpc(method, params_raw) "{\"jsonrpc\":\"2.0\",\"id\":" + id_val + ",\"result\":" + result + "}" } fn route_outline(query: String) -> String { let source: String = query_get(query, "source") lsp_outline(source) } fn route_definition(query: String) -> String { let source: String = query_get(query, "source") let pos: Int = str_to_int(query_get(query, "pos")) lsp_definition(source, pos) } fn route_references(query: String) -> String { let source: String = query_get(query, "source") let pos: Int = str_to_int(query_get(query, "pos")) lsp_references(source, pos) } fn route_format(body: String) -> String { let content: String = json_get_string(body, "content") lsp_format(content) } fn route_type_graph(query: String) -> String { let source: String = query_get(query, "source") lsp_type_graph(source) } // ── Themes ─────────────────────────────────────────────────────────────────── fn active_theme() -> String { let t: String = state_get("ide/active_theme") if str_eq(t, "") { return "dark" } t } fn route_themes_list() -> String { let active: String = active_theme() let names: [String] = native_list_empty() let names = native_list_append(names, "dark") let names = native_list_append(names, "light") let names = native_list_append(names, "neuron") let names = native_list_append(names, "high-contrast") let labels: [String] = native_list_empty() let labels = native_list_append(labels, "Dark") let labels = native_list_append(labels, "Light") let labels = native_list_append(labels, "Neuron") let labels = native_list_append(labels, "High Contrast") let n: Int = native_list_len(names) let out: String = "[" let i: Int = 0 while i < n { if i > 0 { let out = out + "," } let nm: String = native_list_get(names, i) let lbl: String = native_list_get(labels, i) let act: String = "false" if str_eq(nm, active) { let act = "true" } let out = out + "{\"name\":" + jstr(nm) + ",\"label\":" + jstr(lbl) + ",\"active\":" + act + "}" let i = i + 1 } out + "]" } fn route_themes_set(body: String) -> String { let name: String = json_get_string(body, "name") let valid: Bool = false if str_eq(name, "dark") { let valid = true } if str_eq(name, "light") { let valid = true } if str_eq(name, "neuron") { let valid = true } if str_eq(name, "high-contrast") { let valid = true } if !valid { return jerr("unknown theme: " + name) } state_set("ide/active_theme", name) "{\"ok\":true,\"active\":" + jstr(name) + "}" } // ── Plugins (delegate to plugin_* in plugin-host) ──────────────────────────── fn route_plugins_list() -> String { plugin_list_json() } fn route_plugins_install(body: String) -> String { plugin_install(json_get_string(body, "name")) } fn route_plugins_remove(body: String) -> String { plugin_remove(json_get_string(body, "name")) } fn route_plugins_enable(body: String) -> String { plugin_enable(json_get_string(body, "name")) } fn route_plugins_disable(body: String) -> String { plugin_disable(json_get_string(body, "name")) } // ── Snippets ───────────────────────────────────────────────────────────────── fn route_snippets() -> String { let snips: [String] = native_list_empty() let snips = native_list_append(snips, "{\"label\":\"fn\",\"description\":\"Function definition\",\"body\":\"fn ${1:name}(${2:params}) -> ${3:Ret} {\\n\\t${4:body}\\n}\"}") let snips = native_list_append(snips, "{\"label\":\"type\",\"description\":\"Struct\",\"body\":\"type ${1:Name} {\\n\\t${2:field}: ${3:Type},\\n}\"}") let snips = native_list_append(snips, "{\"label\":\"enum\",\"description\":\"Enum\",\"body\":\"enum ${1:Name} {\\n\\t${2:Variant},\\n}\"}") let snips = native_list_append(snips, "{\"label\":\"match\",\"description\":\"Match expression\",\"body\":\"match ${1:expr} {\\n\\t${2:pat} => ${3:body},\\n\\t_ => ${4:default},\\n}\"}") let snips = native_list_append(snips, "{\"label\":\"let\",\"description\":\"Let binding\",\"body\":\"let ${1:name}: ${2:Type} = ${3:value}\"}") let snips = native_list_append(snips, "{\"label\":\"if\",\"description\":\"If expression\",\"body\":\"if ${1:cond} {\\n\\t${2:body}\\n}\"}") let snips = native_list_append(snips, "{\"label\":\"for\",\"description\":\"For loop\",\"body\":\"for ${1:item} in ${2:iter} {\\n\\t${3:body}\\n}\"}") let snips = native_list_append(snips, "{\"label\":\"activate\",\"description\":\"Activate\",\"body\":\"activate ${1:Type}\"}") "[" + list_join(snips, ",") + "]" } // ── Settings (file-backed) ─────────────────────────────────────────────────── fn settings_path() -> String { let home: String = env("HOME") if str_eq(home, "") { let home = "/tmp" } home + "/.el-ide/settings.json" } fn default_settings_json() -> String { "{\"theme\":\"dark\",\"fontSize\":13,\"tabSize\":4,\"wordWrap\":false,\"formatOnSave\":false,\"vimMode\":false,\"minimap\":true,\"elBinaryPath\":\"el\",\"engramUrl\":\"http://localhost:8742\",\"stickyScroll\":true,\"lineNumbers\":true,\"bracketMatching\":true}" } fn route_settings_get() -> String { let s: String = fs_read(settings_path()) if str_eq(s, "") { return default_settings_json() } s } fn route_settings_save(body: String) -> String { // Body is { "settings": {...} } — extract the inner blob. let blob: String = json_get_raw(body, "settings") if str_eq(blob, "") { return jerr("missing settings") } fs_write(settings_path(), blob) blob } fn route_settings_reset() -> String { fs_write(settings_path(), default_settings_json()) default_settings_json() } // ── Build / run (synchronous; no SSE in current http_serve runtime) ────────── // // The Rust crate spawned `el build/run` and streamed output as SSE events. // El's `http_serve` is request/response only — handlers return one string. // We call the el binary, capture stdout via fs_read of a redirected temp // file, and return JSON. Streaming is a runtime gap (see report). fn route_build_or_run(action: String, body: String) -> String { let rel_file: String = json_get_string(body, "file") if str_eq(rel_file, "") { let rel_file = "src/main.el" } let target: String = safe_join(rel_file) if str_eq(target, "") { return jerr("path escapes project root") } // No process-spawn primitive in the runtime; this is a stub that // documents the planned behaviour. See report for runtime gap. "{\"event\":\"info\",\"action\":" + jstr(action) + ",\"file\":" + jstr(rel_file) + ",\"note\":\"el-ide-server: synchronous build requires a runtime exec primitive — el binary not invoked\"}" } fn route_build(body: String) -> String { route_build_or_run("build", body) } fn route_run(body: String) -> String { route_build_or_run("run", body) } // ── Engram reasoning proxy ─────────────────────────────────────────────────── fn route_reason(body: String) -> String { let url: String = cfg_engram_url() + "/api/reason" let resp: String = http_post(url, body) if str_eq(resp, "") { return jerr("engram unreachable") } resp } fn route_activate_preview(query: String) -> String { let type_name: String = query_get(query, "type_name") let q: String = query_get(query, "query") let url: String = cfg_engram_url() + "/api/activate-preview" let body: String = "{\"type_name\":" + jstr(type_name) + ",\"query\":" + jstr(q) + ",\"limit\":5}" let resp: String = http_post(url, body) if str_eq(resp, "") { return "{\"count\":0,\"nodes\":[],\"connected\":false}" } resp } // ── Index page (minimal placeholder; the real bundle lives in ide/index.html) ─ fn route_index() -> String { let path: String = cfg_project_path() + "/ide/index.html" let html: String = fs_read(path) if !str_eq(html, "") { return html } "el-ide

el-ide

Backend running. UI bundle missing at " + path + ".

" } // ── Health ─────────────────────────────────────────────────────────────────── fn route_health() -> String { "{\"status\":\"ok\",\"engine\":\"el-ide-server\"}" } // ── Dispatcher ─────────────────────────────────────────────────────────────── fn handle_request(method: String, path: String, body: String) -> String { let parsed: Map = split_path_query(path) let route: String = parsed["path"] let query: String = parsed["query"] if str_eq(route, "/") { return route_index() } if str_eq(route, "/health") { return route_health() } if str_eq(route, "/api/status") { return route_status() } if str_eq(route, "/api/config") { return route_config() } // Files if str_eq(route, "/api/files") { return route_files_list(query) } if str_eq(route, "/api/file") { if str_eq(method, "GET") { return route_file_read(query) } if str_eq(method, "POST") { return route_file_write(body) } } // Search — minimal stub (real impl needs recursive grep) if str_eq(route, "/api/search") { let q: String = query_get(query, "q") return "{\"query\":" + jstr(q) + ",\"results\":[]}" } // LSP if str_eq(route, "/api/lsp/complete") { return route_lsp_complete(query) } if str_eq(route, "/api/lsp/hover") { return route_lsp_hover(query) } if str_eq(route, "/api/lsp/errors") { return route_lsp_errors(query) } if str_eq(route, "/api/lsp/activate-preview") { return route_activate_preview(query) } if str_eq(route, "/api/lsp/jsonrpc") { return route_lsp_jsonrpc(body) } if str_eq(route, "/api/type-graph") { return route_type_graph(query) } if str_eq(route, "/api/outline") { return route_outline(query) } if str_eq(route, "/api/definition") { return route_definition(query) } if str_eq(route, "/api/references") { return route_references(query) } if str_eq(route, "/api/format") { return route_format(body) } if str_eq(route, "/api/completions/snippet") { return route_snippets() } // Themes if str_eq(route, "/api/themes") { return route_themes_list() } if str_eq(route, "/api/themes/active") { return route_themes_set(body) } // Plugins if str_eq(route, "/api/plugins") { return route_plugins_list() } if str_eq(route, "/api/plugins/install") { return route_plugins_install(body) } if str_eq(route, "/api/plugins/remove") { return route_plugins_remove(body) } if str_eq(route, "/api/plugins/enable") { return route_plugins_enable(body) } if str_eq(route, "/api/plugins/disable") { return route_plugins_disable(body) } // Settings if str_eq(route, "/api/settings") { if str_eq(method, "GET") { return route_settings_get() } if str_eq(method, "POST") { return route_settings_save(body) } if str_eq(method, "DELETE") { return route_settings_reset() } } // Build / run if str_eq(route, "/api/build") { return route_build(body) } if str_eq(route, "/api/run") { return route_run(body) } // Reasoning proxy if str_eq(route, "/api/reason") { return route_reason(body) } // Git — relies on shell exec; runtime gap. Stub returns empty. if str_eq(route, "/api/git/status") { return "[]" } if str_eq(route, "/api/git/diff") { return "\"\"" } "{\"error\":\"not found\",\"path\":" + jstr(route) + "}" } // ── Entry ──────────────────────────────────────────────────────────────────── fn server_main() -> Void { plugin_init() let port: Int = cfg_port() println("[el-ide-server] project=" + cfg_project_path()) println("[el-ide-server] engram=" + cfg_engram_url()) println("[el-ide-server] listening on http://0.0.0.0:" + int_to_str(port)) http_set_handler("handle_request") http_serve(port, "handle_request") } server_main()