rssn-advanced 0.1.2

This is rssn-advanced: The next generation symbolic core of rssn.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
//! Async CFFI bridge for multi-language integration.
//!
//! Provides a callback-based asynchronous C API that allows external
//! languages to invoke long-running simplification, compilation, and
//! one-shot evaluation without blocking the calling thread.
//!
//! Dispatches through the `dtact` fiber pool; the fiber handle is returned
//! to the caller and joined explicitly via a `rssn_async_*_join` function.
//!
//! # Handle types
//!
//! | Handle | Async operation | Join function |
//! |--------|----------------|---------------|
//! | [`RssnAsyncHandle`] | simplify | [`rssn_async_join`] |
//! | [`RssnAsyncCompileHandle`] | simplify → compile | [`rssn_async_compile_join`] |
//! | [`RssnAsyncEvalHandle`] | simplify → compile → execute | [`rssn_async_eval_join`] |
//!
//! # Safety contract
//!
//! The caller must call the corresponding `*_join` function before freeing
//! the `DagBuilder` (and, for eval, the `vars` array) passed to the async
//! function.  Failure to join is a use-after-free.

#![allow(unsafe_code)]

use std::os::raw::c_void;
use std::panic::catch_unwind;

use super::types::RssnStatus;
use crate::dag::builder::DagBuilder;
use crate::dag::node::DagNodeId;
use crate::heuristic::{HeuristicConfig, HeuristicEngine, SearchStrategy};
use crate::runtime::{TaskHandle, ensure_runtime, join, spawn_task};

// =========================================================================
// v2: joinable async handle
// =========================================================================

/// Opaque async simplification handle. Returned by
/// [`rssn_dag_simplify_async`]; pass to [`rssn_async_join`] to block
/// until the fiber completes and obtain the result.
///
/// The handle stores the `TaskHandle` value as a `u64` for C ABI
/// compatibility. A value of `u64::MAX` signals an early error (null
/// builder, etc.).
#[repr(C)]
pub struct RssnAsyncHandle {
    /// Internal fiber handle, or `u64::MAX` on error.
    pub handle_id: u64,
    /// Predicted simplified root (written by the fiber before completion).
    /// Zero until the fiber writes it; read only after [`rssn_async_join`].
    pub simplified_root: u32,
    /// Status code set by the fiber.
    pub status: RssnStatus,
}

/// Fires a simplification fiber and returns an opaque handle.
///
/// The caller must call [`rssn_async_join`] before freeing `builder`, making
/// the use-after-free hazard explicit and auditable from C.
///
/// The returned [`RssnAsyncHandle`] must be freed with [`rssn_async_join`].
#[unsafe(no_mangle)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn rssn_dag_simplify_async(
    builder: *mut DagBuilder,
    root: u32,
) -> *mut RssnAsyncHandle {
    let handle_box = Box::new(RssnAsyncHandle {
        handle_id: u64::MAX,
        simplified_root: u32::MAX,
        status: RssnStatus::NullPointer,
    });
    let handle_ptr = Box::into_raw(handle_box);

    if builder.is_null() {
        // handle_ptr already carries NullPointer status; caller joins immediately.
        return handle_ptr;
    }

    let builder_addr = builder as usize;
    let handle_addr = handle_ptr as usize;

    let gate = ensure_runtime();
    let task = spawn_task(gate, move || {
        let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
            let builder_ref = unsafe { &mut *(builder_addr as *mut DagBuilder) };
            let root_id = DagNodeId::new(root);
            let config = HeuristicConfig::default();
            let mut engine = HeuristicEngine::new(config, SearchStrategy::Greedy);
            engine.simplify(builder_ref, root_id).value()
        }));

        // SAFETY: `handle_ptr` was heap-allocated above and is not freed
        // until `rssn_async_join` is called (which happens after this fiber
        // completes — the join blocks).
        let h = unsafe { &mut *(handle_addr as *mut RssnAsyncHandle) };
        match result {
            Ok(simplified) => {
                h.simplified_root = simplified;
                h.status = RssnStatus::Success;
            }
            Err(_) => {
                h.status = RssnStatus::Panic;
            }
        }
    });

    // Stash the task handle id so `rssn_async_join` can await it.
    // SAFETY: we wrote handle_ptr just above; the fiber hasn't touched it yet
    // because it's racing to acquire builder_ref after we return.
    unsafe {
        (*handle_ptr).handle_id = task.raw_id();
    }

    handle_ptr
}

/// Blocks until the fiber behind `handle` completes, then frees the handle
/// and writes the result to `*out_root` (if non-null).
///
/// Returns the final [`RssnStatus`]. After this call, `builder` may be freed.
///
/// # Safety
///
/// `handle` must have been obtained from [`rssn_dag_simplify_async`] and
/// must not be used after this call.
#[unsafe(no_mangle)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn rssn_async_join(handle: *mut RssnAsyncHandle, out_root: *mut u32) -> RssnStatus {
    if handle.is_null() {
        return RssnStatus::NullPointer;
    }
    let h = unsafe { &*handle };
    if h.handle_id != u64::MAX {
        let task_handle = TaskHandle::from_raw(h.handle_id);
        join(task_handle);
    }
    let h = unsafe { &*handle };
    if !out_root.is_null() {
        unsafe { *out_root = h.simplified_root };
    }
    let status = h.status;
    // Free the handle.
    let _ = unsafe { Box::from_raw(handle) };
    status
}

// =========================================================================
// Async compile handle — simplify then JIT-compile
// =========================================================================
//
// Fires a fiber that runs: dag_to_ast → JitCompiler::compile.
// This amortises the first-compile latency (Cranelift IR generation + linking)
// off the calling thread while keeping the global JIT context mutex held only
// inside the fiber.
//
// The compiled function pointer is stored as a `u64` (raw usize cast) so the
// handle struct is `Send` without additional unsafe markers.

/// Opaque handle returned by [`rssn_dag_compile_async`].
///
/// Pass to [`rssn_async_compile_join`] to block until the fiber finishes and
/// retrieve the compiled function pointer.
///
/// The `fn_ptr_bits` field stores the raw function pointer as a `u64`; it is
/// undefined until after a successful join.
#[repr(C)]
pub struct RssnAsyncCompileHandle {
    /// Internal fiber handle id, or `u64::MAX` on early error.
    pub handle_id: u64,
    /// Compiled function pointer bits (valid only after a successful join).
    /// Cast to `double (*)(const double*)` before calling.
    pub fn_ptr_bits: u64,
    /// Status set by the fiber; valid after join.
    pub status: RssnStatus,
}

// SAFETY: `fn_ptr_bits` is a raw function pointer stored as an integer.
// It is only written by the fiber and only read after `join` — no concurrent
// access.  The `DagBuilder` lifetime is the caller's responsibility.
unsafe impl Send for RssnAsyncCompileHandle {}

/// Simplifies and JIT-compiles an expression asynchronously.
///
/// Fires a fiber that runs `simplify(builder, root)` followed by
/// `JitCompiler::compile(ast)` using the process-level JIT context.
/// Returns immediately with an opaque handle; call [`rssn_async_compile_join`]
/// to block until the fiber finishes and obtain the function pointer.
///
/// The caller **must** call [`rssn_async_compile_join`] before freeing `builder`.
///
/// Returns a non-null handle even on early error (null `builder`) — the handle
/// will have `status = NullPointer` and joining it is a no-op.
///
/// # Safety
///
/// - `builder` must remain valid and unmodified until after
///   [`rssn_async_compile_join`] returns.
/// - The returned handle must be freed by [`rssn_async_compile_join`].
#[cfg(feature = "cranelift-jit")]
#[unsafe(no_mangle)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn rssn_dag_compile_async(
    builder: *mut DagBuilder,
    root: u32,
) -> *mut RssnAsyncCompileHandle {
    let handle_box = Box::new(RssnAsyncCompileHandle {
        handle_id: u64::MAX,
        fn_ptr_bits: 0,
        status: RssnStatus::NullPointer,
    });
    let handle_ptr = Box::into_raw(handle_box);

    if builder.is_null() {
        return handle_ptr;
    }

    let builder_addr = builder as usize;
    let handle_addr = handle_ptr as usize;

    let gate = ensure_runtime();
    let task = spawn_task(gate, move || {
        let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
            let builder_ref = unsafe { &mut *(builder_addr as *mut DagBuilder) };
            let root_id = DagNodeId::new(root);

            // Simplify first to reduce JIT work.
            let config = HeuristicConfig::default();
            let mut engine = HeuristicEngine::new(config, SearchStrategy::Greedy);
            let simplified_id = engine.simplify(builder_ref, root_id);

            // Convert to AST and compile.
            let ast = crate::ast::convert::dag_to_ast(builder_ref.arena(), simplified_id);
            let ctx_mutex = crate::ffi::jit_context::global_jit_ctx();
            let mut ctx = ctx_mutex
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            ctx.compiler_mut().compile(&ast).map(|f| f as usize as u64)
        }));

        let h = unsafe { &mut *(handle_addr as *mut RssnAsyncCompileHandle) };
        match result {
            Ok(Ok(fn_bits)) => {
                h.fn_ptr_bits = fn_bits;
                h.status = RssnStatus::Success;
            }
            Ok(Err(_)) => {
                h.status = RssnStatus::CompilationError;
            }
            Err(_) => {
                h.status = RssnStatus::Panic;
            }
        }
    });

    unsafe { (*handle_ptr).handle_id = task.raw_id() };
    handle_ptr
}

/// Stub for non-JIT builds: returns an immediately-joined error handle.
#[cfg(not(feature = "cranelift-jit"))]
#[unsafe(no_mangle)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn rssn_dag_compile_async(
    _builder: *mut DagBuilder,
    _root: u32,
) -> *mut RssnAsyncCompileHandle {
    Box::into_raw(Box::new(RssnAsyncCompileHandle {
        handle_id: u64::MAX,
        fn_ptr_bits: 0,
        status: RssnStatus::CompilationError,
    }))
}

/// Blocks until the compile fiber completes, writes the function pointer to
/// `*out_fn` (if non-null), frees the handle, and returns the fiber's status.
///
/// After this call `builder` may be freed.  The function pointer written to
/// `*out_fn` (if `status == Success`) is valid for as long as the process-level
/// JIT module lives — i.e. for the lifetime of the process.
///
/// # Safety
///
/// `handle` must have been obtained from [`rssn_dag_compile_async`] and must
/// not be used after this call.
#[unsafe(no_mangle)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn rssn_async_compile_join(
    handle: *mut RssnAsyncCompileHandle,
    out_fn: *mut *mut c_void,
) -> RssnStatus {
    if handle.is_null() {
        return RssnStatus::NullPointer;
    }
    let h = unsafe { &*handle };
    if h.handle_id != u64::MAX {
        let task_handle = TaskHandle::from_raw(h.handle_id);
        join(task_handle);
    }
    let h = unsafe { &*handle };
    if !out_fn.is_null() {
        unsafe { *out_fn = h.fn_ptr_bits as *mut c_void };
    }
    let status = h.status;
    let _ = unsafe { Box::from_raw(handle) };
    status
}

// =========================================================================
// Async one-shot eval handle — simplify, compile, then execute
// =========================================================================
//
// Useful for benchmarks and first-time evaluations where the expression has
// never been compiled before.  The caller provides the variable array up-front;
// the fiber holds a raw pointer to it and the caller must keep it alive until
// after `rssn_async_eval_join` returns.
//
// Result is a `f64` stored in the handle.

/// Opaque handle returned by [`rssn_dag_eval_async`].
///
/// Pass to [`rssn_async_eval_join`] to block until the fiber finishes and
/// retrieve the computed `f64` result.
#[repr(C)]
pub struct RssnAsyncEvalHandle {
    /// Internal fiber handle id, or `u64::MAX` on early error.
    pub handle_id: u64,
    /// Computed result (valid only after a successful join).
    pub result: f64,
    /// Status set by the fiber; valid after join.
    pub status: RssnStatus,
}

// SAFETY: same reasoning as RssnAsyncCompileHandle.
unsafe impl Send for RssnAsyncEvalHandle {}

/// Simplifies, JIT-compiles, and evaluates an expression asynchronously.
///
/// The full pipeline — `simplify → compile → execute(vars)` — runs in a fiber.
/// Returns immediately; call [`rssn_async_eval_join`] to block and get the
/// `f64` result.
///
/// The caller **must** call [`rssn_async_eval_join`] before freeing `builder`
/// **or** `vars`.
///
/// # Safety
///
/// - `builder` must remain valid and unmodified until after
///   [`rssn_async_eval_join`] returns.
/// - `vars` must point to an array of at least as many `f64` values as there
///   are distinct variables in the expression, ordered by `SymbolId`.  It must
///   remain valid and unmodified until after [`rssn_async_eval_join`] returns.
/// - The returned handle must be freed by [`rssn_async_eval_join`].
#[cfg(feature = "cranelift-jit")]
#[unsafe(no_mangle)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn rssn_dag_eval_async(
    builder: *mut DagBuilder,
    root: u32,
    vars: *const f64,
) -> *mut RssnAsyncEvalHandle {
    let handle_box = Box::new(RssnAsyncEvalHandle {
        handle_id: u64::MAX,
        result: f64::NAN,
        status: RssnStatus::NullPointer,
    });
    let handle_ptr = Box::into_raw(handle_box);

    if builder.is_null() || vars.is_null() {
        return handle_ptr;
    }

    let builder_addr = builder as usize;
    let vars_addr = vars as usize;
    let handle_addr = handle_ptr as usize;

    let gate = ensure_runtime();
    let task = spawn_task(gate, move || {
        let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
            let builder_ref = unsafe { &mut *(builder_addr as *mut DagBuilder) };
            let vars_ptr = vars_addr as *const f64;
            let root_id = DagNodeId::new(root);

            // Simplify.
            let config = HeuristicConfig::default();
            let mut engine = HeuristicEngine::new(config, SearchStrategy::Greedy);
            let simplified_id = engine.simplify(builder_ref, root_id);

            // Compile.
            let ast = crate::ast::convert::dag_to_ast(builder_ref.arena(), simplified_id);
            let compiled_fn = {
                let ctx_mutex = crate::ffi::jit_context::global_jit_ctx();
                let mut ctx = ctx_mutex
                    .lock()
                    .unwrap_or_else(std::sync::PoisonError::into_inner);
                ctx.compiler_mut().compile(&ast)?
            };

            // Execute.
            Ok::<f64, crate::error::JitError>(compiled_fn(vars_ptr))
        }));

        let h = unsafe { &mut *(handle_addr as *mut RssnAsyncEvalHandle) };
        match result {
            Ok(Ok(val)) => {
                h.result = val;
                h.status = RssnStatus::Success;
            }
            Ok(Err(_)) => {
                h.status = RssnStatus::CompilationError;
            }
            Err(_) => {
                h.status = RssnStatus::Panic;
            }
        }
    });

    unsafe { (*handle_ptr).handle_id = task.raw_id() };
    handle_ptr
}

/// Stub for non-JIT builds.
#[cfg(not(feature = "cranelift-jit"))]
#[unsafe(no_mangle)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn rssn_dag_eval_async(
    _builder: *mut DagBuilder,
    _root: u32,
    _vars: *const f64,
) -> *mut RssnAsyncEvalHandle {
    Box::into_raw(Box::new(RssnAsyncEvalHandle {
        handle_id: u64::MAX,
        result: f64::NAN,
        status: RssnStatus::CompilationError,
    }))
}

/// Blocks until the eval fiber completes, writes the result to `*out_val`
/// (if non-null), frees the handle, and returns the fiber's status.
///
/// After this call `builder` and `vars` may be freed.
///
/// # Safety
///
/// `handle` must have been obtained from [`rssn_dag_eval_async`] and must
/// not be used after this call.
#[unsafe(no_mangle)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn rssn_async_eval_join(
    handle: *mut RssnAsyncEvalHandle,
    out_val: *mut f64,
) -> RssnStatus {
    if handle.is_null() {
        return RssnStatus::NullPointer;
    }
    let h = unsafe { &*handle };
    if h.handle_id != u64::MAX {
        let task_handle = TaskHandle::from_raw(h.handle_id);
        join(task_handle);
    }
    let h = unsafe { &*handle };
    if !out_val.is_null() {
        unsafe { *out_val = h.result };
    }
    let status = h.status;
    let _ = unsafe { Box::from_raw(handle) };
    status
}