Skip to main content

increparse_lua/
lib.rs

1//! Define an increparse language server in **pure Lua**.
2//!
3//! A language is a single Lua file returning a table:
4//!
5//! ```lua
6//! -- minilang.lua
7//! return {
8//!   name = "minilang",
9//!   root_ctx = { File = true },
10//!   -- round r of a run calls passes[r]
11//!   passes = {
12//!     function(source, span, ctx)
13//!       -- span = { start = , end = , rev = }
14//!       -- return { expand = { { start = , end = , ctx = ... }, ... } }
15//!       --     or "done" / "failed" / nil
16//!       return "done"
17//!     end,
18//!   },
19//!   -- optional: how a failing region becomes a diagnostic
20//!   diagnostic = function(source, node) return { message = "oops" } end,
21//!   -- optional: outline symbols from a snapshot of the tree
22//!   symbols = function(nodes) return {} end,
23//! }
24//! ```
25//!
26//! Hand the file to the bundled binary and point any LSP client at it:
27//!
28//! ```text
29//! increparse-lua-server ~/.config/minilang/lang.lua
30//! ```
31//!
32//! # Semantics
33//!
34//! * Passes receive `(source, span, ctx)` where child ranges in the outcome
35//!   are **absolute** byte ranges into `source` — no rebasing traps.
36//! * Outcomes: a table with an `expand` array expands the region; `"done"`
37//!   accepts it; `"failed"` or `nil` marks it failed (later passes retry).
38//!   A Lua error thrown inside a pass is caught and treated as `Failed` —
39//!   a broken pass never takes the server down.
40//! * Contexts are arbitrary Lua values. Region reuse after an edit matches
41//!   contexts by **deep equality** (tables compared recursively), so edits
42//!   re-parse only the touched chain — the same incremental behaviour the
43//!   Rust side gets.
44//! * `diagnostic` receives `(source, node)` with
45//!   `node = { ctx, status, start, end }` (`status` is `"failed"` or
46//!   `"unparsed"`); returning a table with a `message` publishes a
47//!   diagnostic at the node's range (optionally set `start`/`end` there).
48//! * `symbols` receives an array of `{ start, end, status, ctx }` snapshots
49//!   and returns `{ { name = , detail = , start = , end = } }`; ranges are
50//!   byte ranges, converted to editor positions by the skeleton.
51
52#![forbid(unsafe_code)]
53#![deny(missing_docs)]
54
55use std::error::Error;
56use std::path::Path;
57
58use increparse::{Engine, Outcome, Pass, Schedule, Span};
59use increparse_lsp::{Document, FailedNode, Language};
60use lsp_types::{Diagnostic, DiagnosticSeverity, DocumentSymbol, SymbolKind};
61use mlua::{Function, Lua, Table, Value};
62
63/// A context value from Lua: any [`Value`], compared by deep equality.
64///
65/// Deep equality is what lets the incremental tree reuse regions across
66/// edits: two contexts match only if they are structurally identical
67/// (tables recursively; `1` and `1.0` compare equal; functions and other
68/// opaque values never compare equal — contexts should be data).
69#[derive(Debug, Clone)]
70pub struct LuaCtx(pub Value);
71
72impl PartialEq for LuaCtx {
73    fn eq(&self, other: &Self) -> bool {
74        deep_eq(&self.0, &other.0)
75    }
76}
77
78impl Eq for LuaCtx {}
79
80fn deep_eq(a: &Value, b: &Value) -> bool {
81    match (a, b) {
82        (Value::Nil, Value::Nil) => true,
83        (Value::Boolean(x), Value::Boolean(y)) => x == y,
84        (Value::Integer(x), Value::Integer(y)) => x == y,
85        (Value::Number(x), Value::Number(y)) => x == y,
86        (Value::Integer(x), Value::Number(y)) | (Value::Number(y), Value::Integer(x)) => {
87            (*x as f64) == *y
88        }
89        (Value::String(x), Value::String(y)) => x.as_bytes() == y.as_bytes(),
90        (Value::Table(x), Value::Table(y)) => {
91            let a: Vec<(Value, Value)> = x.pairs().filter_map(|p| p.ok()).collect();
92            let b: Vec<(Value, Value)> = y.pairs().filter_map(|p| p.ok()).collect();
93            if a.len() != b.len() {
94                return false;
95            }
96            a.iter()
97                .all(|(k, v)| b.iter().any(|(k2, v2)| deep_eq(k, k2) && deep_eq(v, v2)))
98        }
99        _ => false,
100    }
101}
102
103fn status_name(status: increparse::Status) -> &'static str {
104    match status {
105        increparse::Status::Unparsed => "unparsed",
106        increparse::Status::Expanded => "expanded",
107        increparse::Status::Done => "done",
108        increparse::Status::Failed => "failed",
109    }
110}
111
112/// A [`Pass`] backed by a Lua function.
113#[derive(Clone)]
114pub(crate) struct LuaPass {
115    lua: Lua,
116    function: Function,
117    index: usize,
118}
119
120impl LuaPass {
121    fn try_parse(
122        &self,
123        source: &str,
124        span: Span,
125        ctx: &LuaCtx,
126    ) -> Result<Outcome<LuaCtx>, Box<dyn Error + Send + Sync>> {
127        let lua = &self.lua;
128        let span_table = lua.create_table()?;
129        span_table.set("start", span.start)?;
130        span_table.set("end", span.end)?;
131        span_table.set("rev", span.rev)?;
132
133        let result: Value = self.function.call((source, span_table, ctx.0.clone()))?;
134
135        match result {
136            Value::Nil => Ok(Outcome::Failed),
137            Value::String(s) if s.as_bytes() == b"done" => Ok(Outcome::Done),
138            Value::String(s) if s.as_bytes() == b"failed" => Ok(Outcome::Failed),
139            Value::Table(outcome) => {
140                let expand: Value = outcome.get("expand")?;
141                let Value::Table(array) = expand else {
142                    eprintln!(
143                        "increparse-lua: pass #{} returned a table without `expand`; treating as failed",
144                        self.index
145                    );
146                    return Ok(Outcome::Failed);
147                };
148                let mut children = Vec::new();
149                for item in array.sequence_values::<Table>() {
150                    let item = item?;
151                    let start: usize = item.get("start")?;
152                    let end: usize = item.get("end")?;
153                    let child_ctx = LuaCtx(item.get::<Value>("ctx")?);
154                    if start > end {
155                        return Err(format!(
156                            "pass #{} produced a child with start {start} > end {end}",
157                            self.index
158                        )
159                        .into());
160                    }
161                    children.push((Span::new(start, end, span.rev), child_ctx));
162                }
163                Ok(Outcome::Expand(children))
164            }
165            other => {
166                eprintln!(
167                    "increparse-lua: pass #{} returned {}; treating as failed",
168                    self.index,
169                    other.type_name()
170                );
171                Ok(Outcome::Failed)
172            }
173        }
174    }
175}
176
177impl Pass for LuaPass {
178    type Ctx = LuaCtx;
179
180    fn parse(&self, source: &str, span: Span, ctx: &LuaCtx) -> Outcome<LuaCtx> {
181        match self.try_parse(source, span, ctx) {
182            Ok(outcome) => outcome,
183            Err(err) => {
184                eprintln!("increparse-lua: pass #{} error: {err}", self.index);
185                Outcome::Failed
186            }
187        }
188    }
189
190    fn name(&self) -> &'static str {
191        "LuaPass"
192    }
193}
194
195/// A language defined by a Lua configuration file.
196///
197/// Build with [`LuaLanguage::from_path`] and hand it to
198/// [`increparse_lsp::serve`](increparse_lsp::serve):
199///
200/// ```no_run
201/// fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
202///     let language = increparse_lua::LuaLanguage::from_path("minilang.lua")?;
203///     increparse_lsp::serve(language)
204/// }
205/// ```
206pub struct LuaLanguage {
207    lua: Lua,
208    engine: Engine<LuaCtx>,
209    root_ctx: LuaCtx,
210    name: String,
211    diagnostic_fn: Option<Function>,
212    symbols_fn: Option<Function>,
213}
214
215impl LuaLanguage {
216    /// Loads a language definition from a Lua file.
217    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, Box<dyn Error + Send + Sync>> {
218        let lua = Lua::new();
219        let script = std::fs::read_to_string(path)?;
220        let config: Value = lua.load(script).eval()?;
221        Self::from_config(lua, config)
222    }
223
224    /// Builds a language from an already-evaluated configuration table.
225    pub fn from_config(lua: Lua, config: Value) -> Result<Self, Box<dyn Error + Send + Sync>> {
226        let Value::Table(table) = &config else {
227            return Err("config must be a table".into());
228        };
229
230        let name: String = table.get("name")?;
231
232        let root_ctx = LuaCtx(table.get("root_ctx")?);
233
234        let passes_table: Table = table.get("passes")?;
235        let mut functions = Vec::new();
236        for f in passes_table.sequence_values::<Function>() {
237            functions.push(f?);
238        }
239        if functions.is_empty() {
240            return Err("config.passes must list at least one function".into());
241        }
242
243        let mut schedule = Schedule::new();
244        for (index, function) in functions.into_iter().enumerate() {
245            schedule.push(LuaPass {
246                lua: lua.clone(),
247                function,
248                index,
249            });
250        }
251        let engine = Engine::new(schedule);
252
253        let diagnostic_fn = match table.get::<Value>("diagnostic")? {
254            Value::Nil => None,
255            Value::Function(f) => Some(f),
256            other => {
257                return Err(format!(
258                    "config.diagnostic must be a function, got {}",
259                    other.type_name()
260                )
261                .into())
262            }
263        };
264        let symbols_fn = match table.get::<Value>("symbols")? {
265            Value::Nil => None,
266            Value::Function(f) => Some(f),
267            other => {
268                return Err(format!(
269                    "config.symbols must be a function, got {}",
270                    other.type_name()
271                )
272                .into())
273            }
274        };
275
276        Ok(Self {
277            lua,
278            engine,
279            root_ctx,
280            name,
281            diagnostic_fn,
282            symbols_fn,
283        })
284    }
285
286    /// The language's `name` from the config.
287    pub fn name(&self) -> &str {
288        &self.name
289    }
290
291    /// The embedded Lua state (contexts live here).
292    pub fn lua(&self) -> &Lua {
293        &self.lua
294    }
295}
296
297impl Language<LuaCtx> for LuaLanguage {
298    fn supports_symbols(&self) -> bool {
299        self.symbols_fn.is_some()
300    }
301
302    fn engine(&self) -> &Engine<LuaCtx> {
303        &self.engine
304    }
305
306    fn root_ctx(&self) -> LuaCtx {
307        self.root_ctx.clone()
308    }
309
310    fn diagnostic(
311        &self,
312        doc: &Document<LuaCtx>,
313        node: FailedNode<'_, LuaCtx>,
314    ) -> Option<Diagnostic> {
315        let function = self.diagnostic_fn.as_ref()?;
316        let span = doc.session().tree().span(node.id);
317        let node_table = self.lua.create_table().ok()?;
318        node_table.set("ctx", node.ctx.0.clone()).ok()?;
319        node_table.set("status", status_name(node.status)).ok()?;
320        node_table.set("start", span.start).ok()?;
321        node_table.set("end", span.end).ok()?;
322
323        let result: Value = function.call((doc.text().to_string(), node_table)).ok()?;
324        let Value::Table(diag) = result else {
325            return None;
326        };
327        let message: String = diag.get("message").ok()?;
328        let severity = diag
329            .get::<i64>("severity")
330            .ok()
331            .and_then(|s| match s {
332                1 => Some(DiagnosticSeverity::ERROR),
333                2 => Some(DiagnosticSeverity::WARNING),
334                3 => Some(DiagnosticSeverity::INFORMATION),
335                4 => Some(DiagnosticSeverity::HINT),
336                _ => None,
337            })
338            .or(Some(DiagnosticSeverity::ERROR));
339
340        Some(Diagnostic {
341            message,
342            severity,
343            ..Diagnostic::default()
344        })
345    }
346
347    fn symbols(&self, doc: &Document<LuaCtx>) -> Vec<DocumentSymbol> {
348        let Some(function) = &self.symbols_fn else {
349            return Vec::new();
350        };
351        let Ok(nodes) = self.lua.create_table() else {
352            return Vec::new();
353        };
354        let tree = doc.session().tree();
355        for id in tree.nodes() {
356            let Ok(entry) = self.lua.create_table() else {
357                return Vec::new();
358            };
359            let span = tree.span(id);
360            if entry.set("start", span.start).is_err()
361                || entry.set("end", span.end).is_err()
362                || entry.set("status", status_name(tree.status(id))).is_err()
363                || entry.set("ctx", tree.ctx(id).0.clone()).is_err()
364            {
365                return Vec::new();
366            }
367            if nodes.push(entry).is_err() {
368                return Vec::new();
369            }
370        }
371
372        let result: Value = match function.call(nodes) {
373            Ok(v) => v,
374            Err(err) => {
375                eprintln!("increparse-lua: symbols error: {err}");
376                return Vec::new();
377            }
378        };
379        let Value::Table(array) = result else {
380            return Vec::new();
381        };
382
383        let mut symbols = Vec::new();
384        for item in array.sequence_values::<Table>() {
385            let Ok(item) = item else { break };
386            let Ok(name) = item.get::<String>("name") else {
387                continue;
388            };
389            let (Ok(start), Ok(end)) = (item.get::<usize>("start"), item.get::<usize>("end"))
390            else {
391                continue;
392            };
393            let detail: Option<String> = item.get("detail").unwrap_or(None);
394            let span = Span::new(start, end, doc.revision());
395            let range = doc.range(span);
396            symbols.push(DocumentSymbol {
397                name,
398                detail,
399                kind: SymbolKind::FUNCTION,
400                range,
401                selection_range: range,
402                children: None,
403                tags: None,
404                #[allow(deprecated)]
405                deprecated: None,
406            });
407        }
408        symbols
409    }
410}