1use crate::marshaling;
7use shape_abi_v1::{LanguageRuntimeLspConfig, PluginError};
8use std::collections::HashMap;
9use std::ffi::c_void;
10
11pub struct CompiledFunction {
13 pub name: String,
15 pub js_source: String,
18 pub param_names: Vec<String>,
20 pub shape_body_start_line: u32,
22 pub is_async: bool,
24 pub return_type: String,
26 pub v8_fn_name: String,
28}
29
30pub struct TsRuntime {
35 js_runtime: deno_core::JsRuntime,
37 functions: HashMap<usize, CompiledFunction>,
39 next_id: usize,
41 tokio_runtime: Option<tokio::runtime::Runtime>,
45}
46
47impl TsRuntime {
48 pub fn new(_config_msgpack: &[u8]) -> Result<Self, String> {
54 let js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
55 ..Default::default()
56 });
57
58 Ok(TsRuntime {
59 js_runtime,
60 functions: HashMap::new(),
61 next_id: 1,
62 tokio_runtime: None,
63 })
64 }
65
66 pub fn register_types(&mut self, _types_msgpack: &[u8]) -> Result<(), String> {
71 Ok(())
75 }
76
77 pub fn compile(
99 &mut self,
100 name: &str,
101 source: &str,
102 param_names: &[String],
103 _param_types: &[String],
104 return_type: &str,
105 is_async: bool,
106 ) -> Result<*mut c_void, String> {
107 let id = self.next_id;
108 self.next_id += 1;
109
110 let v8_fn_name = format!("__shape_ts_{id}");
111 let params_str = param_names.join(", ");
112
113 let indented_body: String = source
115 .lines()
116 .map(|line| format!(" {line}"))
117 .collect::<Vec<_>>()
118 .join("\n");
119
120 let js_source = if is_async {
121 format!("async function {v8_fn_name}({params_str}) {{\n{indented_body}\n}}")
122 } else {
123 format!("function {v8_fn_name}({params_str}) {{\n{indented_body}\n}}")
124 };
125
126 self.js_runtime
128 .execute_script("<shape-ts-compile>", js_source.clone())
129 .map_err(|e| format!("TypeScript compilation error in '{}': {}", name, e))?;
130
131 let func = CompiledFunction {
132 name: name.to_string(),
133 js_source,
134 param_names: param_names.to_vec(),
135 shape_body_start_line: 0,
136 is_async,
137 return_type: return_type.to_string(),
138 v8_fn_name,
139 };
140
141 self.functions.insert(id, func);
142
143 Ok(id as *mut c_void)
145 }
146
147 pub fn invoke(&mut self, handle: *mut c_void, args_msgpack: &[u8]) -> Result<Vec<u8>, String> {
152 let id = handle as usize;
153 let func = self
154 .functions
155 .get(&id)
156 .ok_or_else(|| format!("invalid function handle: {id}"))?;
157
158 let v8_fn_name = func.v8_fn_name.clone();
159 let func_name = func.name.clone();
160 let is_async = func.is_async;
161
162 let arg_values: Vec<rmpv::Value> = if args_msgpack.is_empty() {
164 Vec::new()
165 } else {
166 rmp_serde::from_slice(args_msgpack)
167 .map_err(|e| format!("Failed to deserialize args: {}", e))?
168 };
169
170 let args_js = arg_values
173 .iter()
174 .map(|v| rmpv_to_js_literal(v))
175 .collect::<Vec<_>>()
176 .join(", ");
177
178 let call_expr = if is_async {
179 format!("(async () => await {v8_fn_name}({args_js}))()")
183 } else {
184 format!("{v8_fn_name}({args_js})")
185 };
186
187 if is_async {
188 let result = self
190 .js_runtime
191 .execute_script("<shape-ts-invoke>", call_expr)
192 .map_err(|e| format!("TypeScript error in '{}': {}", func_name, e))?;
193
194 let rt = self.tokio_runtime.get_or_insert(
198 tokio::runtime::Builder::new_current_thread()
199 .enable_all()
200 .build()
201 .map_err(|e| format!("Failed to create async runtime: {}", e))?,
202 );
203
204 let resolved = rt.block_on(async {
205 let resolved = self.js_runtime.resolve(result);
206 self.js_runtime
207 .with_event_loop_promise(resolved, deno_core::PollEventLoopOptions::default())
208 .await
209 });
210
211 let global = resolved
212 .map_err(|e| format!("TypeScript async error in '{}': {}", func_name, e))?;
213
214 let scope = &mut self.js_runtime.handle_scope();
216 let local = deno_core::v8::Local::new(scope, global);
217 marshaling::v8_to_msgpack(scope, local)
218 } else {
219 let result = self
220 .js_runtime
221 .execute_script("<shape-ts-invoke>", call_expr)
222 .map_err(|e| format!("TypeScript error in '{}': {}", func_name, e))?;
223
224 let scope = &mut self.js_runtime.handle_scope();
225 let local = deno_core::v8::Local::new(scope, result);
226 marshaling::v8_to_msgpack(scope, local)
227 }
228 }
229
230 pub fn dispose_function(&mut self, handle: *mut c_void) {
232 let id = handle as usize;
233 if let Some(func) = self.functions.remove(&id) {
234 let delete_script = format!("delete globalThis.{};", func.v8_fn_name);
236 let _ = self
237 .js_runtime
238 .execute_script("<shape-ts-dispose>", delete_script);
239 }
240 }
241
242 pub fn language_id() -> &'static str {
244 "typescript"
245 }
246
247 pub fn lsp_config() -> LanguageRuntimeLspConfig {
249 LanguageRuntimeLspConfig {
250 language_id: "typescript".into(),
251 server_command: vec!["typescript-language-server".into(), "--stdio".into()],
252 file_extension: ".ts".into(),
253 extra_paths: Vec::new(),
254 }
255 }
256}
257
258fn rmpv_to_js_literal(value: &rmpv::Value) -> String {
262 match value {
263 rmpv::Value::Nil => "null".to_string(),
264 rmpv::Value::Boolean(b) => if *b { "true" } else { "false" }.to_string(),
265 rmpv::Value::Integer(i) => {
266 if let Some(n) = i.as_i64() {
267 n.to_string()
268 } else if let Some(n) = i.as_u64() {
269 n.to_string()
270 } else {
271 "0".to_string()
272 }
273 }
274 rmpv::Value::F32(f) => format!("{}", f),
275 rmpv::Value::F64(f) => format!("{}", f),
276 rmpv::Value::String(s) => {
277 if let Some(s) = s.as_str() {
278 format!("\"{}\"", escape_js_string(s))
279 } else {
280 "null".to_string()
281 }
282 }
283 rmpv::Value::Array(arr) => {
284 let items: Vec<String> = arr.iter().map(rmpv_to_js_literal).collect();
285 format!("[{}]", items.join(", "))
286 }
287 rmpv::Value::Map(entries) => {
288 let pairs: Vec<String> = entries
289 .iter()
290 .map(|(k, v)| {
291 let key_str = match k {
292 rmpv::Value::String(s) => {
293 if let Some(s) = s.as_str() {
294 format!("\"{}\"", escape_js_string(s))
295 } else {
296 "\"\"".to_string()
297 }
298 }
299 _ => rmpv_to_js_literal(k),
300 };
301 format!("{}: {}", key_str, rmpv_to_js_literal(v))
302 })
303 .collect();
304 format!("{{{}}}", pairs.join(", "))
305 }
306 rmpv::Value::Binary(b) => {
307 let items: Vec<String> = b.iter().map(|byte| byte.to_string()).collect();
309 format!("new Uint8Array([{}])", items.join(", "))
310 }
311 rmpv::Value::Ext(_, _) => "null".to_string(),
312 }
313}
314
315fn escape_js_string(s: &str) -> String {
317 let mut out = String::with_capacity(s.len());
318 for ch in s.chars() {
319 match ch {
320 '\\' => out.push_str("\\\\"),
321 '"' => out.push_str("\\\""),
322 '\n' => out.push_str("\\n"),
323 '\r' => out.push_str("\\r"),
324 '\t' => out.push_str("\\t"),
325 '\0' => out.push_str("\\0"),
326 c => out.push(c),
327 }
328 }
329 out
330}
331
332pub unsafe extern "C" fn ts_init(config: *const u8, config_len: usize) -> *mut c_void {
337 let config_slice = if config.is_null() || config_len == 0 {
338 &[]
339 } else {
340 unsafe { std::slice::from_raw_parts(config, config_len) }
341 };
342
343 match TsRuntime::new(config_slice) {
344 Ok(runtime) => Box::into_raw(Box::new(runtime)) as *mut c_void,
345 Err(_) => std::ptr::null_mut(),
346 }
347}
348
349pub unsafe extern "C" fn ts_register_types(
350 instance: *mut c_void,
351 types_msgpack: *const u8,
352 types_len: usize,
353) -> i32 {
354 if instance.is_null() {
355 return PluginError::NotInitialized as i32;
356 }
357 let runtime = unsafe { &mut *(instance as *mut TsRuntime) };
358 let types_slice = if types_msgpack.is_null() || types_len == 0 {
359 &[]
360 } else {
361 unsafe { std::slice::from_raw_parts(types_msgpack, types_len) }
362 };
363
364 match runtime.register_types(types_slice) {
365 Ok(()) => PluginError::Success as i32,
366 Err(_) => PluginError::InternalError as i32,
367 }
368}
369
370pub unsafe extern "C" fn ts_compile(
371 instance: *mut c_void,
372 name: *const u8,
373 name_len: usize,
374 source: *const u8,
375 source_len: usize,
376 param_names_msgpack: *const u8,
377 param_names_len: usize,
378 param_types_msgpack: *const u8,
379 param_types_len: usize,
380 return_type: *const u8,
381 return_type_len: usize,
382 is_async: bool,
383 out_error: *mut *mut u8,
384 out_error_len: *mut usize,
385) -> *mut c_void {
386 if instance.is_null() {
387 return std::ptr::null_mut();
388 }
389 let runtime = unsafe { &mut *(instance as *mut TsRuntime) };
390
391 let name_str = match str_from_raw(name, name_len) {
392 Some(s) => s,
393 None => {
394 write_error(out_error, out_error_len, "invalid function name");
395 return std::ptr::null_mut();
396 }
397 };
398 let source_str = match str_from_raw(source, source_len) {
399 Some(s) => s,
400 None => {
401 write_error(out_error, out_error_len, "invalid source text");
402 return std::ptr::null_mut();
403 }
404 };
405 let return_type_str = match str_from_raw(return_type, return_type_len) {
406 Some(s) => s,
407 None => "_",
408 };
409
410 let param_names: Vec<String> = if param_names_msgpack.is_null() || param_names_len == 0 {
411 Vec::new()
412 } else {
413 let slice = unsafe { std::slice::from_raw_parts(param_names_msgpack, param_names_len) };
414 match rmp_serde::from_slice(slice) {
415 Ok(v) => v,
416 Err(_) => {
417 write_error(out_error, out_error_len, "invalid param names msgpack");
418 return std::ptr::null_mut();
419 }
420 }
421 };
422
423 let param_types: Vec<String> = if param_types_msgpack.is_null() || param_types_len == 0 {
424 Vec::new()
425 } else {
426 let slice = unsafe { std::slice::from_raw_parts(param_types_msgpack, param_types_len) };
427 match rmp_serde::from_slice(slice) {
428 Ok(v) => v,
429 Err(_) => {
430 write_error(out_error, out_error_len, "invalid param types msgpack");
431 return std::ptr::null_mut();
432 }
433 }
434 };
435
436 match runtime.compile(
437 name_str,
438 source_str,
439 ¶m_names,
440 ¶m_types,
441 return_type_str,
442 is_async,
443 ) {
444 Ok(handle) => handle,
445 Err(msg) => {
446 write_error(out_error, out_error_len, &msg);
447 std::ptr::null_mut()
448 }
449 }
450}
451
452fn write_error(out_error: *mut *mut u8, out_error_len: *mut usize, msg: &str) {
454 if out_error.is_null() || out_error_len.is_null() {
455 return;
456 }
457 let mut bytes = msg.as_bytes().to_vec();
458 let len = bytes.len();
459 let ptr = bytes.as_mut_ptr();
460 std::mem::forget(bytes);
461 unsafe {
462 *out_error = ptr;
463 *out_error_len = len;
464 }
465}
466
467pub unsafe extern "C" fn ts_invoke(
468 instance: *mut c_void,
469 handle: *mut c_void,
470 args_msgpack: *const u8,
471 args_len: usize,
472 out_ptr: *mut *mut u8,
473 out_len: *mut usize,
474) -> i32 {
475 if instance.is_null() || out_ptr.is_null() || out_len.is_null() {
476 return PluginError::InvalidArgument as i32;
477 }
478 let runtime = unsafe { &mut *(instance as *mut TsRuntime) };
479 let args_slice = if args_msgpack.is_null() || args_len == 0 {
480 &[]
481 } else {
482 unsafe { std::slice::from_raw_parts(args_msgpack, args_len) }
483 };
484
485 match runtime.invoke(handle, args_slice) {
486 Ok(mut bytes) => {
487 let len = bytes.len();
488 let ptr = bytes.as_mut_ptr();
489 std::mem::forget(bytes);
490 unsafe {
491 *out_ptr = ptr;
492 *out_len = len;
493 }
494 PluginError::Success as i32
495 }
496 Err(msg) => {
497 let error_code = if msg.contains("Failed to deserialize")
502 || msg.contains("Failed to serialize")
503 || msg.contains("invalid function handle")
504 {
505 PluginError::InvalidArgument
506 } else {
507 PluginError::InternalError
508 };
509
510 let mut err_bytes = msg.into_bytes();
512 let len = err_bytes.len();
513 let ptr = err_bytes.as_mut_ptr();
514 std::mem::forget(err_bytes);
515 unsafe {
516 *out_ptr = ptr;
517 *out_len = len;
518 }
519 error_code as i32
520 }
521 }
522}
523
524pub unsafe extern "C" fn ts_dispose_function(instance: *mut c_void, handle: *mut c_void) {
525 if instance.is_null() {
526 return;
527 }
528 let runtime = unsafe { &mut *(instance as *mut TsRuntime) };
529 runtime.dispose_function(handle);
530}
531
532pub unsafe extern "C" fn ts_language_id(_instance: *mut c_void) -> *const std::ffi::c_char {
533 c"typescript".as_ptr()
535}
536
537pub unsafe extern "C" fn ts_get_lsp_config(
538 _instance: *mut c_void,
539 out_ptr: *mut *mut u8,
540 out_len: *mut usize,
541) -> i32 {
542 if out_ptr.is_null() || out_len.is_null() {
543 return PluginError::InvalidArgument as i32;
544 }
545 let config = TsRuntime::lsp_config();
546 match rmp_serde::to_vec(&config) {
547 Ok(mut bytes) => {
548 let len = bytes.len();
549 let ptr = bytes.as_mut_ptr();
550 std::mem::forget(bytes);
551 unsafe {
552 *out_ptr = ptr;
553 *out_len = len;
554 }
555 PluginError::Success as i32
556 }
557 Err(_) => PluginError::InternalError as i32,
558 }
559}
560
561pub unsafe extern "C" fn ts_free_buffer(ptr: *mut u8, len: usize) {
562 if !ptr.is_null() && len > 0 {
563 let _ = unsafe { Vec::from_raw_parts(ptr, len, len) };
564 }
565}
566
567pub unsafe extern "C" fn ts_drop(instance: *mut c_void) {
568 if !instance.is_null() {
569 let _ = unsafe { Box::from_raw(instance as *mut TsRuntime) };
570 }
571}
572
573fn str_from_raw<'a>(ptr: *const u8, len: usize) -> Option<&'a str> {
578 if ptr.is_null() || len == 0 {
579 return None;
580 }
581 let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
582 std::str::from_utf8(slice).ok()
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[test]
590 fn lsp_config_exposes_typescript_defaults() {
591 let config = TsRuntime::lsp_config();
592 assert_eq!(config.language_id, "typescript");
593 assert_eq!(
594 config.server_command,
595 vec![
596 "typescript-language-server".to_string(),
597 "--stdio".to_string()
598 ]
599 );
600 assert_eq!(config.file_extension, ".ts");
601 assert!(config.extra_paths.is_empty());
602 }
603
604 #[test]
605 fn ts_get_lsp_config_returns_valid_msgpack_payload() {
606 let mut out_ptr: *mut u8 = std::ptr::null_mut();
607 let mut out_len: usize = 0;
608
609 let code = unsafe { ts_get_lsp_config(std::ptr::null_mut(), &mut out_ptr, &mut out_len) };
610 assert_eq!(code, PluginError::Success as i32);
611 assert!(!out_ptr.is_null());
612 assert!(out_len > 0);
613
614 let bytes = unsafe { std::slice::from_raw_parts(out_ptr, out_len) };
615 let decoded: LanguageRuntimeLspConfig =
616 rmp_serde::from_slice(bytes).expect("payload should decode");
617 assert_eq!(decoded.language_id, "typescript");
618 assert_eq!(decoded.file_extension, ".ts");
619
620 unsafe { ts_free_buffer(out_ptr, out_len) };
621 }
622}