Skip to main content

rex_wasm/
lib.rs

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    // Match CLI semantics by evaluating snippets through module/snippet rewriting.
126    // This avoids behavior differences between the native CLI and wasm playground.
127    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}