1#![forbid(unsafe_code)]
2#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
3
4use futures::executor::block_on;
5use rex_ast::CompilationUnit;
6use rex_engine::{Engine, ValueDisplayOptions};
7use rex_lsp::server::{
8 code_actions_for_source_public, completion_for_source, diagnostics_for_source,
9 document_symbols_for_source_public, format_for_source_public, goto_definition_for_source,
10 hover_for_source, references_for_source_public, rename_for_source_public,
11};
12use rex_parser::{error::ParseError, parse};
13use rex_typesystem::{
14 inference::infer,
15 typesystem::{TypeSystem, TypeSystemLimits},
16};
17use wasm_bindgen::prelude::*;
18
19fn parse_program(source: &str) -> Result<CompilationUnit, String> {
20 parse(source).map_err(|errs| format_parse_errors(&errs))
21}
22
23fn format_parse_errors(errs: &[ParseError]) -> String {
24 let mut out = String::from("parse error:");
25 for err in errs {
26 out.push('\n');
27 out.push_str(" ");
28 out.push_str(&err.to_string());
29 }
30 out
31}
32
33pub fn parse_to_json(source: &str) -> Result<String, String> {
34 let program = parse_program(source)?;
35 serde_json::to_string(&program).map_err(|e| format!("serialization error: {e}"))
36}
37
38pub fn infer_to_json(source: &str) -> Result<String, String> {
39 let program = parse_program(source)?;
40
41 let mut ts = TypeSystem::new_with_prelude().map_err(|e| format!("type system error: {e}"))?;
42 ts.set_limits(TypeSystemLimits::safe_defaults());
43 ts.register_decls(&program.decls)
44 .map_err(|e| format!("type declaration error: {e}"))?;
45
46 let Some(body) = program.body.as_ref() else {
47 return Err("missing final expression".to_string());
48 };
49 let (preds, typ) = infer(&mut ts, body.as_ref()).map_err(|e| format!("type error: {e}"))?;
50
51 let payload = serde_json::json!({
52 "type": typ.to_string(),
53 "predicates": preds
54 .iter()
55 .map(|p| format!("{} {}", p.class, p.typ))
56 .collect::<Vec<_>>(),
57 });
58 serde_json::to_string(&payload).map_err(|e| format!("serialization error: {e}"))
59}
60
61pub fn lsp_diagnostics_to_json(source: &str) -> Result<String, String> {
62 let diagnostics = diagnostics_for_source(source);
63 serde_json::to_string(&diagnostics).map_err(|e| format!("serialization error: {e}"))
64}
65
66pub fn lsp_completions_to_json(source: &str, line: u32, character: u32) -> Result<String, String> {
67 let completions = completion_for_source(source, line, character);
68 serde_json::to_string(&completions).map_err(|e| format!("serialization error: {e}"))
69}
70
71pub fn lsp_hover_to_json(source: &str, line: u32, character: u32) -> Result<String, String> {
72 let hover = hover_for_source(source, line, character);
73 serde_json::to_string(&hover).map_err(|e| format!("serialization error: {e}"))
74}
75
76pub fn lsp_goto_definition_to_json(
77 source: &str,
78 line: u32,
79 character: u32,
80) -> Result<String, String> {
81 let location = goto_definition_for_source(source, line, character);
82 serde_json::to_string(&location).map_err(|e| format!("serialization error: {e}"))
83}
84
85pub fn lsp_references_to_json(
86 source: &str,
87 line: u32,
88 character: u32,
89 include_declaration: bool,
90) -> Result<String, String> {
91 let refs = references_for_source_public(source, line, character, include_declaration);
92 serde_json::to_string(&refs).map_err(|e| format!("serialization error: {e}"))
93}
94
95pub fn lsp_rename_to_json(
96 source: &str,
97 line: u32,
98 character: u32,
99 new_name: &str,
100) -> Result<String, String> {
101 let edit = rename_for_source_public(source, line, character, new_name);
102 serde_json::to_string(&edit).map_err(|e| format!("serialization error: {e}"))
103}
104
105pub fn lsp_document_symbols_to_json(source: &str) -> Result<String, String> {
106 let symbols = document_symbols_for_source_public(source);
107 serde_json::to_string(&symbols).map_err(|e| format!("serialization error: {e}"))
108}
109
110pub fn lsp_format_to_json(source: &str) -> Result<String, String> {
111 let edits = format_for_source_public(source);
112 serde_json::to_string(&edits).map_err(|e| format!("serialization error: {e}"))
113}
114
115pub fn lsp_code_actions_to_json(source: &str, line: u32, character: u32) -> Result<String, String> {
116 let actions = code_actions_for_source_public(source, line, character);
117 serde_json::to_string(&actions).map_err(|e| format!("serialization error: {e}"))
118}
119
120pub async fn eval_to_string(source: &str) -> Result<String, String> {
121 let _ = parse_program(source)?;
122
123 let mut engine = Engine::with_prelude(()).map_err(|e| format!("engine init error: {e}"))?;
124 engine.type_system.set_limits(TypeSystemLimits::unlimited());
125 let (value, _value_ty) = engine
128 .into_evaluator()
129 .eval_snippet(source)
130 .await
131 .map_err(|e| format!("runtime error: {e}"))?;
132
133 value
134 .display_with(ValueDisplayOptions::docs())
135 .map_err(|e| format!("display error: {e}"))
136}
137
138fn as_js_err(err: String) -> JsValue {
139 JsValue::from_str(&err)
140}
141
142#[wasm_bindgen(js_name = parseToJson)]
143pub fn wasm_parse_to_json(source: &str) -> Result<String, JsValue> {
144 parse_to_json(source).map_err(as_js_err)
145}
146
147#[wasm_bindgen(js_name = inferToJson)]
148pub fn wasm_infer_to_json(source: &str) -> Result<String, JsValue> {
149 infer_to_json(source).map_err(as_js_err)
150}
151
152#[wasm_bindgen(js_name = lspDiagnosticsToJson)]
153pub fn wasm_lsp_diagnostics_to_json(source: &str) -> Result<String, JsValue> {
154 lsp_diagnostics_to_json(source).map_err(as_js_err)
155}
156
157#[wasm_bindgen(js_name = lspCompletionsToJson)]
158pub fn wasm_lsp_completions_to_json(
159 source: &str,
160 line: u32,
161 character: u32,
162) -> Result<String, JsValue> {
163 lsp_completions_to_json(source, line, character).map_err(as_js_err)
164}
165
166#[wasm_bindgen(js_name = lspHoverToJson)]
167pub fn wasm_lsp_hover_to_json(source: &str, line: u32, character: u32) -> Result<String, JsValue> {
168 lsp_hover_to_json(source, line, character).map_err(as_js_err)
169}
170
171#[wasm_bindgen(js_name = lspGotoDefinitionToJson)]
172pub fn wasm_lsp_goto_definition_to_json(
173 source: &str,
174 line: u32,
175 character: u32,
176) -> Result<String, JsValue> {
177 lsp_goto_definition_to_json(source, line, character).map_err(as_js_err)
178}
179
180#[wasm_bindgen(js_name = lspReferencesToJson)]
181pub fn wasm_lsp_references_to_json(
182 source: &str,
183 line: u32,
184 character: u32,
185 include_declaration: bool,
186) -> Result<String, JsValue> {
187 lsp_references_to_json(source, line, character, include_declaration).map_err(as_js_err)
188}
189
190#[wasm_bindgen(js_name = lspRenameToJson)]
191pub fn wasm_lsp_rename_to_json(
192 source: &str,
193 line: u32,
194 character: u32,
195 new_name: &str,
196) -> Result<String, JsValue> {
197 lsp_rename_to_json(source, line, character, new_name).map_err(as_js_err)
198}
199
200#[wasm_bindgen(js_name = lspDocumentSymbolsToJson)]
201pub fn wasm_lsp_document_symbols_to_json(source: &str) -> Result<String, JsValue> {
202 lsp_document_symbols_to_json(source).map_err(as_js_err)
203}
204
205#[wasm_bindgen(js_name = lspFormatToJson)]
206pub fn wasm_lsp_format_to_json(source: &str) -> Result<String, JsValue> {
207 lsp_format_to_json(source).map_err(as_js_err)
208}
209
210#[wasm_bindgen(js_name = lspCodeActionsToJson)]
211pub fn wasm_lsp_code_actions_to_json(
212 source: &str,
213 line: u32,
214 character: u32,
215) -> Result<String, JsValue> {
216 lsp_code_actions_to_json(source, line, character).map_err(as_js_err)
217}
218
219#[wasm_bindgen(js_name = evalToJson)]
220pub fn wasm_eval_to_json(source: &str) -> Result<String, JsValue> {
221 let _ = parse_program(source).map_err(as_js_err)?;
222
223 let fut = async move {
224 let engine = Engine::with_prelude(()).map_err(|e| format!("engine init error: {e}"))?;
225 let (value, _value_ty) = engine
226 .into_evaluator()
227 .eval_snippet(source)
228 .await
229 .map_err(|e| format!("runtime error: {e}"))?;
230 let rendered = value
231 .display_with(ValueDisplayOptions::unsanitized())
232 .map_err(|e| format!("display error: {e}"))?;
233 let payload = serde_json::json!({ "value": rendered });
234 serde_json::to_string(&payload).map_err(|e| format!("serialization error: {e}"))
235 };
236 block_on(fut).map_err(as_js_err)
237}
238
239#[wasm_bindgen(js_name = evalToString)]
240pub fn wasm_eval_to_string(source: &str) -> Result<String, JsValue> {
241 block_on(eval_to_string(source)).map_err(as_js_err)
242}
243
244#[cfg(test)]
245mod tests {
246 use super::{
247 eval_to_string, lsp_code_actions_to_json, lsp_diagnostics_to_json, wasm_eval_to_json,
248 };
249 use futures::executor::block_on;
250
251 #[test]
252 fn eval_to_string_hides_snippet_prefix_and_numeric_suffix() {
253 let source = r#"
254type T = A | B;
255let
256 x = A,
257 n = 2
258in
259 (n, [x, B])
260"#;
261 let full = wasm_eval_to_json(source).expect("wasm eval failed");
262 assert!(full.contains("2i32"));
263 assert!(full.contains("A"));
264 assert!(full.contains("B"));
265
266 let sanitized = block_on(eval_to_string(source)).expect("wasm string eval failed");
267 assert_eq!(sanitized, "(2, [A, B])");
268 }
269
270 #[test]
271 fn lsp_diagnostics_preserve_all_unknown_var_usages() {
272 let source = r#"
273let
274 f = \x -> missing + x
275in
276 missing + (f missing)
277"#;
278 let json = lsp_diagnostics_to_json(source).expect("diagnostics json");
279 let diagnostics: serde_json::Value =
280 serde_json::from_str(&json).expect("diagnostics parse");
281 let count = diagnostics
282 .as_array()
283 .expect("diagnostics array")
284 .iter()
285 .filter(|diag| {
286 diag.get("message")
287 .and_then(serde_json::Value::as_str)
288 .is_some_and(|m| m.contains("unbound variable missing"))
289 })
290 .count();
291 assert_eq!(count, 3, "diagnostics: {diagnostics:#?}");
292 }
293
294 #[test]
295 fn lsp_code_actions_include_unknown_var_fixes() {
296 let source = r#"
297let
298 x = 1
299in
300 y + x
301"#;
302 let json = lsp_code_actions_to_json(source, 4, 2).expect("code actions json");
303 let actions: serde_json::Value = serde_json::from_str(&json).expect("actions parse");
304 let has_replace = actions
305 .as_array()
306 .expect("actions array")
307 .iter()
308 .any(|item| {
309 item.get("title")
310 .and_then(serde_json::Value::as_str)
311 .is_some_and(|title| title.contains("Replace `y` with `x`"))
312 });
313 assert!(has_replace, "actions: {actions:#?}");
314 }
315}