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