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