Skip to main content

luaur_web/functions/
check_script.rs

1//! `extern "C" const char* checkScript(const char* source, int useNewSolver)`
2//! (`CLI/src/Web.cpp:142-182`).
3//!
4//! The wasm type-checking entry point: builds a demo `Frontend`, registers the
5//! Luau builtins, type-checks the single module `"main"`, and returns the
6//! newline-joined `line: message` diagnostics (or null when clean). The C++
7//! caches the result in a function-`static std::string` so the returned pointer
8//! outlives the call; the Rust analog is a thread-local `CString`.
9
10use crate::records::demo_config_resolver::DemoConfigResolver;
11use crate::records::demo_file_resolver::DemoFileResolver;
12use alloc::string::{String, ToString};
13use core::cell::RefCell;
14use core::ffi::{c_char, c_int, CStr};
15use std::ffi::CString;
16
17use luaur_analysis::enums::solver_mode::SolverMode;
18use luaur_analysis::functions::freeze::freeze;
19use luaur_analysis::functions::register_builtin_globals::register_builtin_globals;
20use luaur_analysis::functions::to_string_error::to_string_type_error;
21use luaur_analysis::functions::unfreeze::unfreeze;
22use luaur_analysis::records::frontend::Frontend;
23use luaur_analysis::records::frontend_options::FrontendOptions;
24
25thread_local! {
26    /// Mirror of the C++ `static std::string finalCheckResult;` — keeps the
27    /// returned C string alive after the call returns.
28    static FINAL_CHECK_RESULT: RefCell<Option<CString>> = const { RefCell::new(None) };
29}
30
31/// The fallible body, run under `catch_unwind` to reproduce the C++
32/// `try { ... } catch (const std::exception& e) { finalCheckResult = e.what(); }`.
33fn run_check(source: &str, use_new_solver: c_int) -> String {
34    let mut final_check_result = String::new();
35
36    let mut file_resolver = DemoFileResolver::new();
37    let mut config_resolver = DemoConfigResolver::demo_config_resolver();
38    let options = FrontendOptions::default();
39
40    let mut frontend = Frontend::frontend_file_resolver_config_resolver_frontend_options(
41        &mut file_resolver.base,
42        &mut config_resolver.base,
43        &options,
44    );
45    unsafe {
46        frontend.wire_self_pointers();
47    }
48
49    frontend.set_luau_solver_mode(if use_new_solver != 0 {
50        SolverMode::New
51    } else {
52        SolverMode::Old
53    });
54
55    // Add Luau builtins:
56    //   Luau::unfreeze(frontend.globals.globalTypes);
57    //   Luau::registerBuiltinGlobals(frontend, frontend.globals);
58    //   Luau::freeze(frontend.globals.globalTypes);
59    let frontend_ptr = &mut frontend as *mut Frontend;
60    unsafe {
61        unfreeze((*frontend_ptr).globals.global_types_mut());
62        register_builtin_globals(&mut *frontend_ptr, &mut (*frontend_ptr).globals, false);
63        freeze((*frontend_ptr).globals.global_types_mut());
64    }
65
66    // restart
67    //   frontend.clear();
68    //   fileResolver.source.clear();
69    frontend.clear();
70    file_resolver.source.clear();
71
72    // fileResolver.source["main"] = source;
73    file_resolver
74        .source
75        .insert("main".to_string(), source.to_string());
76
77    // Luau::CheckResult checkResult = frontend.check("main");
78    let check_result =
79        frontend.check_module_name_optional_frontend_options(&"main".to_string(), None);
80
81    for err in &check_result.errors {
82        if !final_check_result.is_empty() {
83            final_check_result.push('\n');
84        }
85        // std::to_string(err.location.begin.line + 1)
86        final_check_result.push_str(&(err.location.begin.line + 1).to_string());
87        final_check_result.push_str(": ");
88        // Luau::toString(err)
89        final_check_result.push_str(&to_string_type_error(err));
90    }
91
92    final_check_result
93}
94
95/// # Safety
96/// `source` must be a valid, NUL-terminated C string (the wasm/JS caller's
97/// contract), or null.
98#[cfg_attr(not(test), no_mangle)]
99pub unsafe extern "C" fn check_script(
100    source: *const c_char,
101    use_new_solver: c_int,
102) -> *const c_char {
103    let source_str = if source.is_null() {
104        String::new()
105    } else {
106        core::str::from_utf8_unchecked(CStr::from_ptr(source).to_bytes()).to_string()
107    };
108
109    // try { ... } catch (const std::exception& e) { finalCheckResult = e.what(); }
110    let final_check_result =
111        match std::panic::catch_unwind(move || run_check(&source_str, use_new_solver)) {
112            Ok(result) => result,
113            Err(payload) => panic_message(&payload),
114        };
115
116    if final_check_result.is_empty() {
117        FINAL_CHECK_RESULT.with(|r| *r.borrow_mut() = None);
118        return core::ptr::null();
119    }
120
121    FINAL_CHECK_RESULT.with(|r| {
122        let cstring = CString::new(final_check_result).unwrap_or_default();
123        let ptr = cstring.as_ptr();
124        *r.borrow_mut() = Some(cstring);
125        ptr
126    })
127}
128
129/// Extract a `std::exception::what()`-equivalent message from a caught panic
130/// payload.
131fn panic_message(payload: &(dyn core::any::Any + Send)) -> String {
132    if let Some(s) = payload.downcast_ref::<&str>() {
133        s.to_string()
134    } else if let Some(s) = payload.downcast_ref::<String>() {
135        s.clone()
136    } else {
137        "unknown error".to_string()
138    }
139}