@@ -23,9 +23,13 @@ use crate::typechecker::TypeChecker;
2323#[ cfg( target_arch = "wasm32" ) ]
2424use crate :: formatter:: format_code;
2525#[ cfg( target_arch = "wasm32" ) ]
26+ use crate :: number:: Value ;
27+ #[ cfg( target_arch = "wasm32" ) ]
2628use crate :: Interpreter ;
2729#[ cfg( target_arch = "wasm32" ) ]
2830use wasm_bindgen:: prelude:: * ;
31+ #[ cfg( target_arch = "wasm32" ) ]
32+ use js_sys:: { Array , Function } ;
2933
3034// ---------------------------------------------------------------------------
3135// Stateful WASM interface: JtvWasm
@@ -311,6 +315,97 @@ impl JtvWasm {
311315 pub fn reset ( & mut self ) {
312316 self . interpreter . reset ( ) ;
313317 }
318+
319+ // =======================================================================
320+ // Coprocessor (extern coproc) — JS callback registration
321+ // =======================================================================
322+
323+ /// Return a JSON array describing extern coproc functions the program
324+ /// declared, so the JS host knows which callbacks to register.
325+ ///
326+ /// Each element has shape `{ "gate": "<gate>", "fn": "<fn_name>" }`.
327+ /// The host must call `register_coproc_impl` for each listed function
328+ /// before executing code that calls it, or execution will throw
329+ /// `ExternCoprocNotYetLowered`.
330+ #[ wasm_bindgen]
331+ pub fn list_coproc_decls ( & self ) -> Result < String , JsValue > {
332+ let decls: Vec < serde_json:: Value > = self
333+ . interpreter
334+ . list_coproc_decls ( )
335+ . into_iter ( )
336+ . map ( |( gate, fn_name) | {
337+ serde_json:: json!( { "gate" : gate, "fn" : fn_name } )
338+ } )
339+ . collect ( ) ;
340+ serde_json:: to_string ( & decls)
341+ . map_err ( |e| JsValue :: from_str ( & format ! ( "{}" , e) ) )
342+ }
343+
344+ /// Register a JavaScript function as the implementation for an extern
345+ /// coproc function named `fn_name`.
346+ ///
347+ /// The JS callback receives each JtV argument as a JS number or string
348+ /// and must return a number (interpreted as `Int`) or a string.
349+ ///
350+ /// Once registered, calls to `fn_name` inside JtV programs will dispatch
351+ /// to the JS callback instead of raising `ExternCoprocNotYetLowered`.
352+ #[ wasm_bindgen]
353+ pub fn register_coproc_impl ( & mut self , fn_name : & str , callback : Function ) {
354+ // SAFETY: wasm32 is single-threaded; no thread boundary is crossed.
355+ struct JsCallback ( Function ) ;
356+ unsafe impl Send for JsCallback { }
357+ unsafe impl Sync for JsCallback { }
358+
359+ let cb = JsCallback ( callback) ;
360+ self . interpreter . register_coproc_impl ( fn_name, move |args| {
361+ let js_args = Array :: new ( ) ;
362+ for arg in args {
363+ js_args. push ( & jtv_value_to_js ( arg) ) ;
364+ }
365+ let result = cb
366+ . 0
367+ . apply ( & JsValue :: NULL , & js_args)
368+ . map_err ( |e| crate :: error:: JtvError :: RuntimeError (
369+ format ! ( "JS coproc callback error: {:?}" , e) ,
370+ ) ) ?;
371+ js_to_jtv_value ( & result)
372+ } ) ;
373+ }
374+ }
375+
376+ // ---------------------------------------------------------------------------
377+ // Value ↔ JsValue helpers (WASM only)
378+ // ---------------------------------------------------------------------------
379+
380+ /// Convert a JtV Value to a JavaScript value for passing into a JS callback.
381+ #[ cfg( target_arch = "wasm32" ) ]
382+ fn jtv_value_to_js ( v : & Value ) -> JsValue {
383+ match v {
384+ Value :: Int ( n) => JsValue :: from_f64 ( * n as f64 ) ,
385+ Value :: Float ( f) => JsValue :: from_f64 ( * f) ,
386+ Value :: String ( s) => JsValue :: from_str ( s) ,
387+ Value :: Bool ( b) => JsValue :: from_bool ( * b) ,
388+ other => JsValue :: from_str ( & format ! ( "{}" , other) ) ,
389+ }
390+ }
391+
392+ /// Convert a JavaScript return value into a JtV Value.
393+ ///
394+ /// Numbers become `Value::Int` (truncated). Strings become `Value::String`.
395+ /// Everything else is stringified.
396+ #[ cfg( target_arch = "wasm32" ) ]
397+ fn js_to_jtv_value ( v : & JsValue ) -> crate :: error:: Result < Value > {
398+ if let Some ( n) = v. as_f64 ( ) {
399+ return Ok ( Value :: Int ( n as i64 ) ) ;
400+ }
401+ if let Some ( s) = v. as_string ( ) {
402+ return Ok ( Value :: String ( s) ) ;
403+ }
404+ if let Some ( b) = v. as_bool ( ) {
405+ return Ok ( Value :: Bool ( b) ) ;
406+ }
407+ // Fallback: convert to string representation
408+ Ok ( Value :: String ( format ! ( "{:?}" , v) ) )
314409}
315410
316411// ---------------------------------------------------------------------------
@@ -568,6 +663,89 @@ mod tests {
568663 assert_eq ! ( format!( "{}" , last. unwrap( ) ) , "42" ) ;
569664 }
570665
666+ // =======================================================================
667+ // Coprocessor / native impl tests (exercises the same code path that
668+ // the WASM JS registration uses, without requiring a wasm32 target)
669+ // =======================================================================
670+
671+ #[ test]
672+ fn test_list_coproc_decls_empty_when_no_extern ( ) {
673+ let code = "x = 1" ;
674+ let program = crate :: parse_program ( code) . unwrap ( ) ;
675+ let mut interp = Interpreter :: new ( ) ;
676+ interp. run ( & program) . unwrap ( ) ;
677+ assert ! ( interp. list_coproc_decls( ) . is_empty( ) ) ;
678+ }
679+
680+ #[ test]
681+ fn test_native_impl_registered_and_called ( ) {
682+ // Register a native impl for "add_one", then call it from JtV.
683+ let code = r#"
684+ extern coproc math_gate {
685+ @pure intrinsic add_one(n: Int): Int ;
686+ }
687+ result = add_one(41)
688+ "# ;
689+ let program = crate :: parse_program ( code) . unwrap ( ) ;
690+ let mut interp = Interpreter :: new ( ) ;
691+ // Resolve coproc blocks without PataCL (None env = all live)
692+ let env = crate :: coproc:: CoprocEnv :: from_triple ( "x86_64-unknown-linux-gnu" , & [ ] ) ;
693+ let ( program, ns) = crate :: coproc:: resolve_coproc_blocks ( program, & env, None ) . unwrap ( ) ;
694+ interp. register_coproc_namespace ( ns) ;
695+ // Register native impl: add_one(n) = n + 1
696+ interp. register_coproc_impl ( "add_one" , |args| {
697+ if let crate :: number:: Value :: Int ( n) = args[ 0 ] {
698+ Ok ( crate :: number:: Value :: Int ( n + 1 ) )
699+ } else {
700+ Err ( crate :: error:: JtvError :: RuntimeError ( "expected Int" . into ( ) ) )
701+ }
702+ } ) ;
703+ interp. run ( & program) . unwrap ( ) ;
704+ let result = interp. get_variable ( "result" ) . unwrap ( ) ;
705+ assert_eq ! ( result, crate :: number:: Value :: Int ( 42 ) ) ;
706+ }
707+
708+ #[ test]
709+ fn test_list_coproc_decls_reports_registered_fns ( ) {
710+ let code = r#"
711+ extern coproc math_gate {
712+ @pure intrinsic add_one(n: Int): Int ;
713+ @pure intrinsic double(n: Int): Int ;
714+ }
715+ "# ;
716+ let program = crate :: parse_program ( code) . unwrap ( ) ;
717+ let env = crate :: coproc:: CoprocEnv :: from_triple ( "x86_64-unknown-linux-gnu" , & [ ] ) ;
718+ let ( _program, ns) = crate :: coproc:: resolve_coproc_blocks ( program, & env, None ) . unwrap ( ) ;
719+ let mut interp = Interpreter :: new ( ) ;
720+ interp. register_coproc_namespace ( ns) ;
721+ let decls = interp. list_coproc_decls ( ) ;
722+ let fn_names: Vec < & str > = decls. iter ( ) . map ( |( _, f) | f. as_str ( ) ) . collect ( ) ;
723+ assert ! ( fn_names. contains( & "add_one" ) , "expected add_one in {:?}" , fn_names) ;
724+ assert ! ( fn_names. contains( & "double" ) , "expected double in {:?}" , fn_names) ;
725+ // Both should have gate name "math_gate"
726+ for ( gate, _) in & decls {
727+ assert_eq ! ( gate, "math_gate" ) ;
728+ }
729+ }
730+
731+ #[ test]
732+ fn test_unregistered_coproc_fn_errors ( ) {
733+ let code = r#"
734+ extern coproc math_gate {
735+ @pure intrinsic add_one(n: Int): Int ;
736+ }
737+ result = add_one(1)
738+ "# ;
739+ let program = crate :: parse_program ( code) . unwrap ( ) ;
740+ let env = crate :: coproc:: CoprocEnv :: from_triple ( "x86_64-unknown-linux-gnu" , & [ ] ) ;
741+ let ( program, ns) = crate :: coproc:: resolve_coproc_blocks ( program, & env, None ) . unwrap ( ) ;
742+ let mut interp = Interpreter :: new ( ) ;
743+ interp. register_coproc_namespace ( ns) ;
744+ // No native impl registered — should error
745+ let result = interp. run ( & program) ;
746+ assert ! ( result. is_err( ) , "Expected ExternCoprocNotYetLowered error" ) ;
747+ }
748+
571749 #[ test]
572750 fn test_output_buffer_drain_on_take ( ) {
573751 let code1 = "print(1)" ;
0 commit comments