Skip to main content

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