use std::str::FromStr;
use std::sync::RwLock;
use wasm_bindgen::prelude::*;
use web_sys::console;
use web_time::Instant;
use crate::lsp::{
WorkspaceState, compute_diagnostics, create_documents, get_semantic_token_legend,
handle_completion_workspace, handle_document_symbols, handle_hover_workspace,
handle_semantic_tokens,
};
use crate::{Compiler, parse_source, parse_source_simple};
use lsp_types::Uri;
static WORKSPACE: RwLock<Option<WorkspaceState>> = RwLock::new(None);
fn log(msg: &str) {
console::log_1(&msg.into());
}
#[wasm_bindgen]
pub fn wasm_init(num_threads: usize) -> js_sys::Promise {
let num_threads = num_threads.min(8);
crate::ir::transform::flatten::enable_cache();
log(&format!(
"[WASM] Flatten cache enabled, initializing thread pool with {} threads",
num_threads
));
wasm_bindgen_rayon::init_thread_pool(num_threads)
}
#[wasm_bindgen]
pub fn get_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[wasm_bindgen]
pub fn test_echo(input: &str) -> String {
format!("Echo: {}", input)
}
#[wasm_bindgen]
pub fn test_grammar() -> String {
use crate::modelica_grammar::ModelicaGrammar;
log("[WASM] test_grammar: creating grammar");
let _grammar = ModelicaGrammar::new();
log("[WASM] test_grammar: grammar created");
"Grammar created successfully".to_string()
}
#[wasm_bindgen]
pub fn parse_modelica(source: &str) -> bool {
log("[WASM] parse_modelica: starting");
log(&format!(
"[WASM] parse_modelica: source length = {}",
source.len()
));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
log("[WASM] parse_modelica: calling parse_source_simple");
let parsed = parse_source_simple(source, "<wasm>");
log("[WASM] parse_modelica: parse_source_simple returned");
parsed.is_some()
}));
match result {
Ok(success) => {
log(&format!("[WASM] parse_modelica: result = {}", success));
success
}
Err(e) => {
log(&format!(
"[WASM] parse_modelica: PANIC: {:?}",
e.downcast_ref::<&str>()
));
false
}
}
}
#[wasm_bindgen]
pub fn compile_to_json(source: &str, model_name: &str) -> Result<String, JsError> {
log("[WASM] compile_to_json: starting");
log(&format!(
"[WASM] compile_to_json: model = {}, source length = {}",
model_name,
source.len()
));
let source_preview: String = source.chars().take(200).collect();
log(&format!(
"[WASM] compile_to_json: source preview: {}",
source_preview
));
log("[WASM] compile_to_json: creating compiler");
let compiler = Compiler::new().model(model_name).cache(false);
log("[WASM] compile_to_json: calling compile_str");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
compiler.compile_str(source, "<wasm>")
}));
handle_compile_result(result)
}
#[derive(serde::Serialize)]
struct LoadLibrariesResult {
parsed_count: usize,
error_count: usize,
library_names: Vec<String>,
conflicts: Vec<String>,
skipped_files: Vec<String>,
}
#[wasm_bindgen]
pub fn load_libraries(libraries_json: &str) -> Result<String, JsError> {
log("[WASM] load_libraries: starting");
let libraries: std::collections::HashMap<String, String> = serde_json::from_str(libraries_json)
.map_err(|e| JsError::new(&format!("Failed to parse libraries JSON: {}", e)))?;
log(&format!(
"[WASM] load_libraries: {} files to parse",
libraries.len()
));
let start = Instant::now();
let mut workspace = WorkspaceState::new();
let result = workspace.load_library_sources(libraries);
let elapsed = start.elapsed();
log(&format!(
"[WASM] load_libraries: parsed {} files in {:.2}s ({} errors, {} skipped, libraries: {:?})",
result.parsed_count,
elapsed.as_secs_f64(),
result.error_count,
result.skipped_files.len(),
result.library_names
));
for (lib_name, lib) in workspace.libraries() {
let class_names: Vec<_> = lib.class_list.keys().take(10).collect();
log(&format!(
"[WASM] Library '{}' has {} classes, first 10: {:?}",
lib_name,
lib.class_list.len(),
class_names
));
}
let mut ws_lock = WORKSPACE
.write()
.map_err(|e| JsError::new(&format!("Failed to acquire workspace lock: {}", e)))?;
*ws_lock = Some(workspace);
let json_result = LoadLibrariesResult {
parsed_count: result.parsed_count,
error_count: result.error_count,
library_names: result.library_names,
conflicts: result.conflicts,
skipped_files: result.skipped_files,
};
serde_json::to_string(&json_result)
.map_err(|e| JsError::new(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen]
pub fn parse_library_file(source: &str, filename: &str) -> Result<String, JsError> {
match parse_source(source, filename) {
Ok(def) => serde_json::to_string(&def)
.map_err(|e| JsError::new(&format!("Serialization error: {}", e))),
Err(e) => Err(JsError::new(&format!("Parse error: {}", e))),
}
}
#[wasm_bindgen]
pub fn merge_parsed_libraries(definitions_json: &str) -> Result<u32, JsError> {
log("[WASM] merge_parsed_libraries: starting");
let definitions_array: Vec<(String, String)> = serde_json::from_str(definitions_json)
.map_err(|e| JsError::new(&format!("Failed to parse definitions JSON: {}", e)))?;
log(&format!(
"[WASM] merge_parsed_libraries: {} definitions to merge",
definitions_array.len()
));
let start = Instant::now();
let mut parsed: Vec<(String, crate::ir::ast::StoredDefinition)> = Vec::new();
let mut errors = 0;
for (filename, ast_json) in definitions_array {
match serde_json::from_str(&ast_json) {
Ok(def) => parsed.push((filename, def)),
Err(e) => {
log(&format!(
"[WASM] merge_parsed_libraries: failed to deserialize {}: {}",
filename, e
));
errors += 1;
}
}
}
log(&format!(
"[WASM] merge_parsed_libraries: deserialized {} definitions ({} errors)",
parsed.len(),
errors
));
let mut workspace = WorkspaceState::new();
let (library_names, conflicts) = workspace.load_library_definitions(parsed);
let elapsed = start.elapsed();
log(&format!(
"[WASM] merge_parsed_libraries: merged {} libraries ({:?}) in {:.2}s{}",
library_names.len(),
library_names,
elapsed.as_secs_f64(),
if conflicts.is_empty() {
String::new()
} else {
format!(" (conflicts: {:?})", conflicts)
}
));
let mut ws_lock = WORKSPACE
.write()
.map_err(|e| JsError::new(&format!("Failed to acquire workspace lock: {}", e)))?;
*ws_lock = Some(workspace);
Ok(library_names.len() as u32)
}
#[wasm_bindgen]
pub fn clear_library_cache() {
log("[WASM] clear_library_cache: clearing");
if let Ok(mut ws) = WORKSPACE.write() {
*ws = None;
}
}
#[wasm_bindgen]
pub fn get_library_count() -> u32 {
WORKSPACE
.read()
.ok()
.and_then(|ws| ws.as_ref().map(|w| w.library_count() as u32))
.unwrap_or(0)
}
#[wasm_bindgen]
pub fn compile_with_libraries(
source: &str,
model_name: &str,
libraries_json: &str, ) -> Result<String, JsError> {
log("[WASM] compile_with_libraries: starting");
let cache_count = get_library_count();
if cache_count > 0 {
log(&format!(
"[WASM] compile_with_libraries: using {} cached libraries",
cache_count
));
compile_with_cached_libraries(source, model_name)
} else {
log("[WASM] compile_with_libraries: no cache, parsing libraries");
compile_with_libraries_uncached(source, model_name, libraries_json)
}
}
fn compile_with_cached_libraries(source: &str, model_name: &str) -> Result<String, JsError> {
let source_preview: String = source.chars().take(200).collect();
log(&format!(
"[WASM] compile_with_cached_libraries: source preview: {}",
source_preview
));
let main_def = parse_source(source, "<wasm>").map_err(|e| JsError::new(&e.to_string()))?;
let ws_lock = WORKSPACE
.read()
.map_err(|e| JsError::new(&format!("Failed to acquire workspace lock: {}", e)))?;
let workspace = ws_lock
.as_ref()
.ok_or_else(|| JsError::new("Workspace not initialized"))?;
log(&format!(
"[WASM] compile_with_cached_libraries: using workspace with {} libraries",
workspace.library_count()
));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
workspace.compile(&main_def, model_name)
}));
handle_compile_result(result)
}
fn compile_with_libraries_uncached(
source: &str,
model_name: &str,
libraries_json: &str,
) -> Result<String, JsError> {
log(&format!(
"[WASM] compile_with_libraries_uncached: model = {}, source length = {}, libraries length = {}",
model_name,
source.len(),
libraries_json.len()
));
let libraries: std::collections::HashMap<String, String> = serde_json::from_str(libraries_json)
.map_err(|e| JsError::new(&format!("Failed to parse libraries JSON: {}", e)))?;
log(&format!(
"[WASM] compile_with_libraries_uncached: {} library files",
libraries.len()
));
let lib_vec: Vec<(&str, &str)> = libraries
.iter()
.map(|(name, src)| (name.as_str(), src.as_str()))
.collect();
let compiler = Compiler::new().model(model_name).cache(false);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
compiler.compile_str_with_sources(source, "<wasm>", lib_vec)
}));
handle_compile_result(result)
}
#[derive(serde::Serialize)]
struct WasmCompileResult {
dae: serde_json::Value,
dae_native: serde_json::Value,
balance: crate::dae::balance::BalanceResult,
pretty: String,
}
fn handle_compile_result(
result: std::result::Result<
anyhow::Result<crate::CompilationResult>,
Box<dyn std::any::Any + Send>,
>,
) -> Result<String, JsError> {
match result {
Ok(Ok(r)) => {
log("[WASM] compile: succeeded, getting JSON");
match r.to_dae_ir_json() {
Ok(dae_json) => {
let dae: serde_json::Value = serde_json::from_str(&dae_json)
.map_err(|e| JsError::new(&format!("DAE JSON parse error: {}", e)))?;
let dae_native: serde_json::Value = serde_json::to_value(&r.dae)
.map_err(|e| JsError::new(&format!("DAE native JSON error: {}", e)))?;
let pretty = r.dae.to_pretty_string();
let combined = WasmCompileResult {
dae,
dae_native,
balance: r.balance,
pretty,
};
let json = serde_json::to_string(&combined)
.map_err(|e| JsError::new(&format!("JSON serialize error: {}", e)))?;
log(&format!("[WASM] compile: JSON length = {}", json.len()));
Ok(json)
}
Err(e) => {
log(&format!("[WASM] compile: JSON error: {}", e));
Err(JsError::new(&e.to_string()))
}
}
}
Ok(Err(e)) => {
log(&format!("[WASM] compile: error: {}", e));
Err(JsError::new(&e.to_string()))
}
Err(panic_info) => {
let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic (likely stack overflow with large libraries)".to_string()
};
log(&format!("[WASM] compile: PANIC: {}", panic_msg));
Err(JsError::new(&format!(
"Compilation panicked: {}",
panic_msg
)))
}
}
}
#[wasm_bindgen]
pub fn lsp_diagnostics(source: &str) -> Result<String, JsError> {
let start = Instant::now();
log("[WASM] lsp_diagnostics: starting");
let uri = Uri::from_str("file:///wasm/model.mo")
.map_err(|e| JsError::new(&format!("Invalid URI: {}", e)))?;
let mut ws_lock = WORKSPACE
.write()
.map_err(|e| JsError::new(&format!("Failed to acquire workspace lock: {}", e)))?;
let workspace = ws_lock.get_or_insert_with(WorkspaceState::new);
workspace.add_document(uri.clone(), source.to_string());
let lib_names: Vec<_> = workspace
.libraries()
.map(|(name, _)| name.as_str())
.collect();
log(&format!(
"[WASM] lsp_diagnostics: setup took {:?}, has {} libraries: {:?}",
start.elapsed(),
workspace.library_count(),
lib_names
));
let diag_start = Instant::now();
let diagnostics = compute_diagnostics(&uri, source, workspace);
log(&format!(
"[WASM] lsp_diagnostics: found {} diagnostics in {:?} (total {:?})",
diagnostics.len(),
diag_start.elapsed(),
start.elapsed()
));
let json = serde_json::to_string(&diagnostics)
.map_err(|e| JsError::new(&format!("JSON serialization error: {}", e)))?;
Ok(json)
}
#[wasm_bindgen]
pub fn lsp_hover(source: &str, line: u32, character: u32) -> Result<String, JsError> {
use lsp_types::{HoverParams, Position, TextDocumentIdentifier, TextDocumentPositionParams};
log(&format!(
"[WASM] lsp_hover: line={}, char={}",
line, character
));
let uri = Uri::from_str("file:///wasm/model.mo")
.map_err(|e| JsError::new(&format!("Invalid URI: {}", e)))?;
let params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position { line, character },
},
work_done_progress_params: Default::default(),
};
let mut ws_lock = WORKSPACE
.write()
.map_err(|e| JsError::new(&format!("Failed to acquire workspace lock: {}", e)))?;
let workspace = ws_lock.get_or_insert_with(WorkspaceState::new);
workspace.add_document(uri, source.to_string());
let hover = handle_hover_workspace(workspace, params);
let json = serde_json::to_string(&hover)
.map_err(|e| JsError::new(&format!("JSON serialization error: {}", e)))?;
Ok(json)
}
#[wasm_bindgen]
pub fn lsp_completion(source: &str, line: u32, character: u32) -> Result<String, JsError> {
use lsp_types::{
CompletionParams, Position, TextDocumentIdentifier, TextDocumentPositionParams,
};
log(&format!(
"[WASM] lsp_completion: line={}, char={}",
line, character
));
let uri = Uri::from_str("file:///wasm/model.mo")
.map_err(|e| JsError::new(&format!("Invalid URI: {}", e)))?;
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position { line, character },
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: None,
};
let mut ws_lock = WORKSPACE
.write()
.map_err(|e| JsError::new(&format!("Failed to acquire workspace lock: {}", e)))?;
let workspace = ws_lock.get_or_insert_with(WorkspaceState::new);
workspace.add_document(uri.clone(), source.to_string());
log(&format!(
"[WASM] lsp_completion: workspace has cached_ast={}",
workspace.get_cached_ast(&uri).is_some()
));
let completions = handle_completion_workspace(workspace, params);
let json = serde_json::to_string(&completions)
.map_err(|e| JsError::new(&format!("JSON serialization error: {}", e)))?;
Ok(json)
}
#[wasm_bindgen]
pub fn lsp_document_symbols(source: &str) -> Result<String, JsError> {
use lsp_types::{DocumentSymbolParams, TextDocumentIdentifier};
log("[WASM] lsp_document_symbols: starting");
let uri = Uri::from_str("file:///wasm/model.mo")
.map_err(|e| JsError::new(&format!("Invalid URI: {}", e)))?;
let documents = create_documents(&uri, source);
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let symbols = handle_document_symbols(&documents, params);
let json = serde_json::to_string(&symbols)
.map_err(|e| JsError::new(&format!("JSON serialization error: {}", e)))?;
Ok(json)
}
#[wasm_bindgen]
pub fn lsp_semantic_tokens(source: &str) -> Result<String, JsError> {
use lsp_types::{SemanticTokensParams, TextDocumentIdentifier};
log("[WASM] lsp_semantic_tokens: starting");
let uri = Uri::from_str("file:///wasm/model.mo")
.map_err(|e| JsError::new(&format!("Invalid URI: {}", e)))?;
let documents = create_documents(&uri, source);
let params = SemanticTokensParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let tokens = handle_semantic_tokens(&documents, params);
let json = serde_json::to_string(&tokens)
.map_err(|e| JsError::new(&format!("JSON serialization error: {}", e)))?;
Ok(json)
}
#[wasm_bindgen]
pub fn lsp_semantic_token_legend() -> Result<String, JsError> {
let legend = get_semantic_token_legend();
let json = serde_json::to_string(&legend)
.map_err(|e| JsError::new(&format!("JSON serialization error: {}", e)))?;
Ok(json)
}
#[wasm_bindgen]
pub fn render_template(dae_json: &str, template: &str) -> Result<String, JsError> {
log("[WASM] render_template: starting");
let dae: crate::dae::ast::Dae = serde_json::from_str(dae_json)
.map_err(|e| JsError::new(&format!("Failed to parse DAE JSON: {}", e)))?;
log(&format!(
"[WASM] render_template: DAE model_name = {}",
dae.model_name
));
let result = crate::dae::jinja::render_template_str(&dae, template)
.map_err(|e| JsError::new(&format!("Template error: {}", e)))?;
log(&format!(
"[WASM] render_template: output length = {}",
result.len()
));
Ok(result)
}