Fix WASM exception handling and SM memory access end-to-end

ober

334155a722f1b6ed23310a4d75c517f44c73835d

diff --git a/jerboa-native-rs/src/wasm_sm.rs b/jerboa-native-rs/src/wasm_sm.rs
index 0f93d53..8d2bc36 100644
--- a/jerboa-native-rs/src/wasm_sm.rs
+++ b/jerboa-native-rs/src/wasm_sm.rs
@@ -44,6 +44,10 @@ struct CallContext {
     instance_handle: u64,
     /// Whether this is a hosted instance (has DNS/WASI imports)
     hosted: bool,
+    /// Pointer to WASM linear memory (set after instantiation, before call)
+    memory_ptr: *mut u8,
+    /// Current size of WASM linear memory in bytes
+    memory_len: usize,
 }
 
 thread_local! {
@@ -115,6 +119,44 @@ struct SmWasmInstance {
 // Host import native functions (called by SpiderMonkey when WASM invokes imports)
 // ============================================================
 
+/// Read bytes from WASM linear memory via the thread-local CallContext.
+/// Returns None if out of bounds or no memory available.
+fn read_wasm_memory(offset: usize, len: usize) -> Option<Vec<u8>> {
+    CALL_CTX.with(|ctx| {
+        let borrow = ctx.borrow();
+        let call_ctx = borrow.as_ref()?;
+        if call_ctx.memory_ptr.is_null() || offset + len > call_ctx.memory_len {
+            return None;
+        }
+        let mut buf = vec![0u8; len];
+        unsafe {
+            ::std::ptr::copy_nonoverlapping(
+                call_ctx.memory_ptr.add(offset), buf.as_mut_ptr(), len);
+        }
+        Some(buf)
+    })
+}
+
+/// Write bytes to WASM linear memory via the thread-local CallContext.
+/// Returns false if out of bounds or no memory available.
+fn write_wasm_memory(offset: usize, data: &[u8]) -> bool {
+    CALL_CTX.with(|ctx| {
+        let borrow = ctx.borrow();
+        let call_ctx = match borrow.as_ref() {
+            Some(c) => c,
+            None => return false,
+        };
+        if call_ctx.memory_ptr.is_null() || offset + data.len() > call_ctx.memory_len {
+            return false;
+        }
+        unsafe {
+            ::std::ptr::copy_nonoverlapping(
+                data.as_ptr(), call_ctx.memory_ptr.add(offset), data.len());
+        }
+        true
+    })
+}
+
 // ---- log_message(level, msg_ptr, msg_len) -> 0 ----
 unsafe extern "C" fn host_log_message(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool {
     let args = CallArgs::from_vp(vp, argc);
@@ -124,10 +166,14 @@ unsafe extern "C" fn host_log_message(_cx: *mut JSContext, argc: u32, vp: *mut V
 
     let lvl = match level { 0 => "ERROR", 1 => "WARN", 2 => "INFO", _ => "DEBUG" };
 
-    // We can't easily read WASM memory from here without the instance object.
-    // Log the raw pointer info for now; full memory access requires refactoring.
-    let msg = format!("[ptr={},len={}]", msg_ptr, msg_len);
-    eprintln!("[wasm-sm-{lvl}] {msg}");
+    let msg = if msg_len > 0 {
+        match read_wasm_memory(msg_ptr as usize, msg_len as usize) {
+            Some(bytes) => String::from_utf8_lossy(&bytes).to_string(),
+            None => format!("[ptr={},len={}]", msg_ptr, msg_len),
+        }
+    } else {
+        String::new()
+    };
 
     CALL_CTX.with(|ctx| {
         if let Some(ref call_ctx) = *ctx.borrow() {
@@ -165,7 +211,24 @@ unsafe extern "C" fn host_get_time_ms(_cx: *mut JSContext, argc: u32, vp: *mut V
 // ---- random_get(buf_ptr, buf_len) -> errno ----
 unsafe extern "C" fn host_random_get(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool {
     let args = CallArgs::from_vp(vp, argc);
-    // Can't write to WASM memory without ArrayBuffer access — return success (no-op)
+    let buf_ptr = if argc > 0 && args.get(0).get().is_int32() { args.get(0).get().to_int32() as usize } else { 0 };
+    let buf_len = if argc > 1 && args.get(1).get().is_int32() { args.get(1).get().to_int32() as usize } else { 0 };
+
+    if buf_len > 0 {
+        let mut rand_buf = vec![0u8; buf_len];
+        // Simple PRNG: fill with pseudo-random bytes based on time
+        let seed = ::std::time::SystemTime::now()
+            .duration_since(::std::time::UNIX_EPOCH)
+            .unwrap_or_default()
+            .as_nanos() as u64;
+        let mut state = seed;
+        for byte in rand_buf.iter_mut() {
+            state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
+            *byte = (state >> 33) as u8;
+        }
+        write_wasm_memory(buf_ptr, &rand_buf);
+    }
+
     args.rval().set(Int32Value(0));
     true
 }
@@ -173,8 +236,33 @@ unsafe extern "C" fn host_random_get(_cx: *mut JSContext, argc: u32, vp: *mut Va
 // ---- fd_write(fd, iovs_ptr, iovs_len, nwritten_ptr) -> errno ----
 unsafe extern "C" fn host_fd_write(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool {
     let args = CallArgs::from_vp(vp, argc);
-    // Stub: return 0 (success) — full implementation requires WASM memory access
-    args.rval().set(Int32Value(0));
+    let _fd = if argc > 0 && args.get(0).get().is_int32() { args.get(0).get().to_int32() } else { 1 };
+    let iovs_ptr = if argc > 1 && args.get(1).get().is_int32() { args.get(1).get().to_int32() as usize } else { 0 };
+    let iovs_len = if argc > 2 && args.get(2).get().is_int32() { args.get(2).get().to_int32() as usize } else { 0 };
+    let nwritten_ptr = if argc > 3 && args.get(3).get().is_int32() { args.get(3).get().to_int32() as usize } else { 0 };
+
+    let mut total_written = 0u32;
+
+    // Read iov entries: each is (ptr: i32, len: i32) = 8 bytes
+    for i in 0..iovs_len {
+        let iov_offset = iovs_ptr + i * 8;
+        if let Some(iov_bytes) = read_wasm_memory(iov_offset, 8) {
+            let data_ptr = u32::from_le_bytes([iov_bytes[0], iov_bytes[1], iov_bytes[2], iov_bytes[3]]) as usize;
+            let data_len = u32::from_le_bytes([iov_bytes[4], iov_bytes[5], iov_bytes[6], iov_bytes[7]]) as usize;
+            if let Some(data) = read_wasm_memory(data_ptr, data_len) {
+                // Write to stderr (fd 1 = stdout, fd 2 = stderr)
+                let _ = ::std::io::Write::write_all(&mut ::std::io::stderr(), &data);
+                total_written += data_len as u32;
+            }
+        }
+    }
+
+    // Write total bytes written
+    if nwritten_ptr > 0 {
+        write_wasm_memory(nwritten_ptr, &total_written.to_le_bytes());
+    }
+
+    args.rval().set(Int32Value(0)); // success
     true
 }
 
@@ -188,6 +276,17 @@ unsafe extern "C" fn host_fd_read(_cx: *mut JSContext, argc: u32, vp: *mut Value
 // ---- clock_time_get(clock_id, precision, time_ptr) -> errno ----
 unsafe extern "C" fn host_clock_time_get(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool {
     let args = CallArgs::from_vp(vp, argc);
+    let _clock_id = if argc > 0 && args.get(0).get().is_int32() { args.get(0).get().to_int32() } else { 0 };
+    let time_ptr = if argc > 2 && args.get(2).get().is_int32() { args.get(2).get().to_int32() as usize } else { 0 };
+
+    if time_ptr > 0 {
+        let nanos = ::std::time::SystemTime::now()
+            .duration_since(::std::time::UNIX_EPOCH)
+            .unwrap_or_default()
+            .as_nanos() as u64;
+        write_wasm_memory(time_ptr, &nanos.to_le_bytes());
+    }
+
     args.rval().set(Int32Value(0));
     true
 }
@@ -313,11 +412,39 @@ unsafe fn sm_compile_and_call(
         return Err("WebAssembly.Instance creation failed".into());
     }
 
-    // Get exports.funcName
+    // Get exports object
     rooted!(&in(cx) let mut exports_val = UndefinedValue());
     JS_GetProperty(cx, instance_obj.handle(), c"exports".as_ptr(), exports_val.handle_mut());
     rooted!(&in(cx) let exports_obj = exports_val.to_object());
 
+    // Extract WASM linear memory pointer for host imports
+    {
+        rooted!(&in(cx) let mut mem_val = UndefinedValue());
+        JS_GetProperty(cx, exports_obj.handle(), c"memory".as_ptr(), mem_val.handle_mut());
+        if !mem_val.get().is_undefined() && mem_val.get().is_object() {
+            // memory is a WebAssembly.Memory — get its .buffer (ArrayBuffer)
+            rooted!(&in(cx) let mem_obj = mem_val.to_object());
+            rooted!(&in(cx) let mut buf_val = UndefinedValue());
+            JS_GetProperty(cx, mem_obj.handle(), c"buffer".as_ptr(), buf_val.handle_mut());
+            if !buf_val.get().is_undefined() && buf_val.get().is_object() {
+                let ab = buf_val.to_object();
+                let mut len: usize = 0;
+                let mut is_shared = false;
+                let mut data: *mut u8 = ptr::null_mut();
+                GetArrayBufferLengthAndData(ab, &mut len, &mut is_shared, &mut data);
+                if !data.is_null() {
+                    CALL_CTX.with(|ctx| {
+                        if let Some(ref mut call_ctx) = *ctx.borrow_mut() {
+                            call_ctx.memory_ptr = data;
+                            call_ctx.memory_len = len;
+                        }
+                    });
+                }
+            }
+        }
+    }
+
+    // Get exported function
     let c_name = ::std::ffi::CString::new(func_name)
         .map_err(|_| "invalid function name".to_string())?;
     rooted!(&in(cx) let mut func_val = UndefinedValue());
@@ -511,7 +638,12 @@ pub extern "C" fn jerboa_sm_call(
 
         // Set thread-local call context for host imports
         CALL_CTX.with(|ctx| {
-            *ctx.borrow_mut() = Some(CallContext { instance_handle, hosted });
+            *ctx.borrow_mut() = Some(CallContext {
+                instance_handle,
+                hosted,
+                memory_ptr: ptr::null_mut(),
+                memory_len: 0,
+            });
         });
 
         // Create a fresh SpiderMonkey runtime for this call
diff --git a/lib/jerboa/wasm/codegen.sls b/lib/jerboa/wasm/codegen.sls
index f3bc196..8ff8e9d 100644
--- a/lib/jerboa/wasm/codegen.sls
+++ b/lib/jerboa/wasm/codegen.sls
@@ -404,13 +404,14 @@
       (encode-function-section (wasm-module-functions mod))
       (encode-table-section (wasm-module-tables mod))
       (encode-memory-section (wasm-module-memories mod))
+      ;; Tag section (13) goes between memory (5) and global (6) per spec
+      (encode-tag-section (wasm-module-tags mod))
       (encode-global-section (wasm-module-globals mod))
       (encode-export-section (wasm-module-exports mod))
       (encode-start-section (wasm-module-start mod))
       (encode-element-section (wasm-module-elements mod))
       (encode-code-section (wasm-module-functions mod))
-      (encode-data-section (wasm-module-data-segments mod))
-      (encode-tag-section (wasm-module-tags mod))))
+      (encode-data-section (wasm-module-data-segments mod))))
 
   ;;; ========== Type conversion ==========
 
@@ -1114,27 +1115,49 @@
               (bytevector wasm-opcode-throw) (encode-u32-leb128 (car args)))]
 
            ;; (try-catch tag-idx body catch-var handler)
-           ;; Emits legacy exceptions try/catch: evaluates body; if an exception
-           ;; with tag-idx is thrown, binds the first payload value to catch-var
-           ;; and evaluates handler.  Both branches must produce an i32 result.
+           ;; Emits phase-4 exception handling using try_table:
+           ;;
+           ;;   block $handler (result i32)      ;; overall result
+           ;;     block $catch                    ;; catch target (no result; payload on stack)
+           ;;       try_table (result i32) (catch tag $catch)
+           ;;         <body>
+           ;;       end
+           ;;       br $handler                   ;; normal path exits to outer block
+           ;;     end
+           ;;     ;; catch path: exception payload i32 on stack
+           ;;     local.set cvar
+           ;;     <handler>
+           ;;   end
            [(try-catch)
             (let* ([tag-idx  (car args)]
                    [body     (cadr args)]
                    [cvar     (caddr args)]
                    [handler  (cadddr args)]
-                   ;; Allocate a local for the caught value
                    [cvar-idx (context-add-local! ctx cvar)])
               (bv-concat
-                ;; try block returning i32
-                (bytevector wasm-opcode-try wasm-type-i32)
+                ;; block $handler (result i32) — outer block, overall result
+                (bytevector wasm-opcode-block wasm-type-i32)
+                ;; block $catch — catch target (receives i32 payload from tag)
+                (bytevector wasm-opcode-block wasm-type-i32)
+                ;; try_table (result i32) with 1 catch clause
+                (bytevector wasm-opcode-try-table wasm-type-i32)
+                (encode-u32-leb128 1)         ;; 1 catch clause
+                (bytevector wasm-catch-kind)   ;; catch
+                (encode-u32-leb128 tag-idx)    ;; tag index
+                (encode-u32-leb128 1)          ;; label depth 1 = $catch block
+                ;; body (normal path)
                 (compile-expr body ctx)
-                ;; catch: pops the payload (first i32) into local cvar
-                (bytevector wasm-opcode-catch)
-                (encode-u32-leb128 tag-idx)
+                (bytevector wasm-opcode-end)   ;; end try_table
+                ;; Normal path: result on stack, branch to outer block
+                (bytevector wasm-opcode-br)
+                (encode-u32-leb128 1)          ;; br 1 = exit to $handler block
+                (bytevector wasm-opcode-end)   ;; end $catch block
+                ;; Catch path: exception payload (i32) is on stack from catch clause
                 (bytevector wasm-opcode-local-set)
                 (encode-u32-leb128 cvar-idx)
                 (compile-expr handler ctx)
-                (bytevector wasm-opcode-end)))]
+                (bytevector wasm-opcode-end)   ;; end $handler block
+                ))]
 
            ;; -- GC: struct operations --
            ;; (struct.new type-idx field-exprs...)
diff --git a/lib/jerboa/wasm/format.sls b/lib/jerboa/wasm/format.sls
index ad0e113..19d462a 100644
--- a/lib/jerboa/wasm/format.sls
+++ b/lib/jerboa/wasm/format.sls
@@ -164,6 +164,8 @@
     ;; ---- Post-MVP: Exception handling opcodes ----
     wasm-opcode-try wasm-opcode-catch wasm-opcode-throw
     wasm-opcode-rethrow wasm-opcode-delegate wasm-opcode-catch-all
+    wasm-opcode-try-table wasm-opcode-throw-ref
+    wasm-catch-kind wasm-catch-ref-kind wasm-catch-all-kind wasm-catch-all-ref-kind
 
     ;; ---- Post-MVP: Typed select ----
     wasm-opcode-select-t
@@ -499,6 +501,7 @@
 
   ;;; ========== Post-MVP: Exception handling opcodes ==========
 
+  ;; Legacy exception handling (wasmi-era, deprecated)
   (define wasm-opcode-try        #x06)
   (define wasm-opcode-catch      #x07)
   (define wasm-opcode-throw      #x08)
@@ -506,6 +509,15 @@
   (define wasm-opcode-delegate   #x18)
   (define wasm-opcode-catch-all  #x19)
 
+  ;; Phase 4 exception handling (try_table + exnref, supported by browsers)
+  (define wasm-opcode-try-table  #x1F)
+  (define wasm-opcode-throw-ref  #x0A)
+  ;; Catch clause opcodes (inside try_table immediates)
+  (define wasm-catch-kind        #x00)  ;; catch tag label
+  (define wasm-catch-ref-kind    #x01)  ;; catch_ref tag label
+  (define wasm-catch-all-kind    #x02)  ;; catch_all label
+  (define wasm-catch-all-ref-kind #x03) ;; catch_all_ref label
+
   ;;; ========== Post-MVP: Typed select ==========
 
   (define wasm-opcode-select-t   #x1C)
diff --git a/lib/std/secure/wasm-target.sls b/lib/std/secure/wasm-target.sls
index 0d62131..2cb51a9 100644
--- a/lib/std/secure/wasm-target.sls
+++ b/lib/std/secure/wasm-target.sls
@@ -1138,7 +1138,10 @@
            [closure-assignment (assign-closure-indices lifted)]
            [lifted (car closure-assignment)]
            [element-forms (cdr closure-assignment)]
-           [has-closures (has-closures? lifted)])
+           [has-closures (has-closures? lifted)]
+           [has-exceptions (has-exceptions? lifted)]
+           ;; Exception tag type index: after closure types (0,1,2 if present)
+           [exc-type-idx (if has-closures 3 0)])
 
       ;; Assemble the complete program
       (append
@@ -1148,6 +1151,14 @@
           (closure-type-forms)
           '())
 
+        ;; 0b. Exception tag type + tag declaration (when try-catch/throw used)
+        ;; Tag type: (i32) -> () — exception payload is a single i32
+        ;; Tag index 0 references this type
+        (if has-exceptions
+          `((define-type (i32) ())
+            (define-tag ,exc-type-idx))
+          '())
+
         ;; 1. Memory and globals
         value-memory-forms
         value-global-forms
@@ -1328,6 +1339,18 @@
       (for-each walk forms)
       found))
 
+  ;; Check if any form uses exception handling (try-catch or throw)
+  (define (has-exceptions? forms)
+    (let ([found #f])
+      (define (walk expr)
+        (when (pair? expr)
+          (when (memq (car expr) '(try-catch throw))
+            (set! found #t))
+          (unless found
+            (for-each walk expr))))
+      (for-each walk forms)
+      found))
+
   ;; Import runtime forms from scheme-runtime module
   (define (runtime-forms)
     ;; These are loaded at compile time from the scheme-runtime module
diff --git a/tests/test-wasm-sandbox.ss b/tests/test-wasm-sandbox.ss
index cdce12d..06da756 100644
--- a/tests/test-wasm-sandbox.ss
+++ b/tests/test-wasm-sandbox.ss
@@ -429,7 +429,8 @@
         value-memory-forms
         value-global-forms
         value-tag-forms
-        '((define-tag 0)
+        '((define-type (i32) ())   ;; type 0: exception payload (i32) -> ()
+          (define-tag 0)            ;; tag 0 references type 0
           (define (safe-div a b)
             (try-catch 0
               (if (= b 0) (throw 0 -1) (quotient a b))
@@ -437,15 +438,17 @@
               exn))))))
   #t)
 
-(test "try-catch wasmi rejects with clear error (exception-handling not supported)"
+(test "try-catch wasmi rejects (exception-handling not supported)"
   (guard (exn [#t (and (message-condition? exn)
-                       (string-contains (condition-message exn) "legacy exceptions") #t)])
+                       (string-contains (condition-message exn) "exception")
+                       #t)])
     (let* ([bv (compile-program
                  (append
                    value-memory-forms
                    value-global-forms
                    value-tag-forms
-                   '((define-tag 0)
+                   '((define-type (i32) ())   ;; type 0: exception payload (i32) -> ()
+                     (define-tag 0)            ;; tag 0 references type 0
                      (define (safe-div a b)
                        (try-catch 0
                          (if (= b 0) (throw 0 -1) (quotient a b))
@@ -682,6 +685,51 @@
         r))
     7)
 
+  ;; Exception handling: try-catch in SpiderMonkey
+  (test "try-catch normal path in SpiderMonkey"
+    (let* ([bv (compile-program
+                 (append
+                   value-memory-forms
+                   value-global-forms
+                   value-tag-forms
+                   '((define-type (i32) ())   ;; exception tag type
+                     (define-tag 0)
+                     (define (safe-compute a b)
+                       (try-catch 0
+                         (+ a b)      ;; normal: returns sum
+                         exn
+                         -1)))))]     ;; catch: returns -1
+           [mod-h (wasm-sandbox-load bv)]
+           [inst (wasm-sandbox-instantiate mod-h)])
+      (let ([r (wasm-sandbox-call inst "safe-compute" 10 20)])
+        (wasm-sandbox-free inst)
+        (wasm-sandbox-free-module mod-h)
+        r))
+    30)
+
+  ;; Exception handling: throw + catch in SpiderMonkey
+  (test "throw + catch in SpiderMonkey"
+    (let* ([bv (compile-program
+                 (append
+                   value-memory-forms
+                   value-global-forms
+                   value-tag-forms
+                   '((define-type (i32) ())   ;; exception tag type
+                     (define-tag 0)
+                     (define (throw-test x)
+                       (try-catch 0
+                         (if (= x 0) (throw 0 99) (+ x 1))
+                         exn
+                         exn)))))]   ;; catch returns the exception value
+           [mod-h (wasm-sandbox-load bv)]
+           [inst (wasm-sandbox-instantiate mod-h)])
+      (let* ([normal (wasm-sandbox-call inst "throw-test" 5)]
+             [caught (wasm-sandbox-call inst "throw-test" 0)])
+        (wasm-sandbox-free inst)
+        (wasm-sandbox-free-module mod-h)
+        (list normal caught)))
+    '(6 99))
+
 )
 
 ;;; ============================================================