Merge pull request 'test(symclaw-skill): cover handlers_advanced via JSON API' (#10) from ci-doctor/coverage-20260518-201834 into master
Reviewed-on: #10
This commit is contained in:
@@ -0,0 +1,486 @@
|
||||
//! C FFI bindings for SymClaw.
|
||||
//!
|
||||
//! Exposes SymClaw's symbolic math engine as C-callable functions.
|
||||
//!
|
||||
//! # Usage from C
|
||||
//!
|
||||
//! ```c
|
||||
//! #include "symclaw.h"
|
||||
//! symclaw_expr_t* e = symclaw_parse("x^2 + 1");
|
||||
//! symclaw_expr_t* d = symclaw_differentiate(e, "x");
|
||||
//! char* s = symclaw_to_string(d);
|
||||
//! printf("%s\n", s);
|
||||
//! symclaw_free_string(s);
|
||||
//! symclaw_free_expr(d);
|
||||
//! symclaw_free_expr(e);
|
||||
//! ```
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
use std::ptr;
|
||||
|
||||
use symclaw_core::ast::Expr;
|
||||
use symclaw_core::codegen;
|
||||
use symclaw_core::differentiate;
|
||||
use symclaw_core::eval;
|
||||
use symclaw_core::integrate;
|
||||
use symclaw_core::interner::Symbol;
|
||||
use symclaw_core::latex;
|
||||
use symclaw_core::limits;
|
||||
use symclaw_core::parser;
|
||||
use symclaw_core::series;
|
||||
use symclaw_core::simplify;
|
||||
use symclaw_core::solve;
|
||||
|
||||
/// Opaque expression handle exposed to C.
|
||||
pub struct SymclawExpr {
|
||||
inner: Expr,
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/// Convert a C string pointer to a Rust `&str`. Returns `None` on null or invalid UTF-8.
|
||||
unsafe fn cstr_to_str<'a>(s: *const c_char) -> Option<&'a str> {
|
||||
if s.is_null() {
|
||||
return None;
|
||||
}
|
||||
unsafe { CStr::from_ptr(s) }.to_str().ok()
|
||||
}
|
||||
|
||||
/// Box an `Expr` into a heap-allocated `SymclawExpr` pointer.
|
||||
fn box_expr(e: Expr) -> *mut SymclawExpr {
|
||||
Box::into_raw(Box::new(SymclawExpr { inner: e }))
|
||||
}
|
||||
|
||||
/// Convert a Rust `String` into a C-owned string pointer.
|
||||
fn string_to_c(s: String) -> *mut c_char {
|
||||
match CString::new(s) {
|
||||
Ok(cs) => cs.into_raw(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Safely borrow the inner `Expr` from a pointer. Returns `None` on null.
|
||||
unsafe fn borrow_expr<'a>(p: *const SymclawExpr) -> Option<&'a Expr> {
|
||||
if p.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { &(*p).inner })
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lifecycle ────────────────────────────────────────────────────
|
||||
|
||||
/// Parse a mathematical expression string into an opaque handle.
|
||||
/// Returns `NULL` on parse failure or null input.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_parse(input: *const c_char) -> *mut SymclawExpr {
|
||||
let s = match unsafe { cstr_to_str(input) } {
|
||||
Some(s) => s,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
match parser::parse(s) {
|
||||
Ok(arc) => box_expr((*arc).clone()),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Free an expression handle. Safe to call with `NULL`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_free_expr(expr: *mut SymclawExpr) {
|
||||
if !expr.is_null() {
|
||||
drop(unsafe { Box::from_raw(expr) });
|
||||
}
|
||||
}
|
||||
|
||||
/// Free a string returned by SymClaw. Safe to call with `NULL`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_free_string(s: *mut c_char) {
|
||||
if !s.is_null() {
|
||||
drop(unsafe { CString::from_raw(s) });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Conversion ──────────────────────────────────────────────────
|
||||
|
||||
/// Convert an expression to its ASCII string representation.
|
||||
/// Returns `NULL` on null input.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_to_string(expr: *const SymclawExpr) -> *mut c_char {
|
||||
match unsafe { borrow_expr(expr) } {
|
||||
Some(e) => string_to_c(format!("{e}")),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an expression to LaTeX.
|
||||
/// Returns `NULL` on null input.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_to_latex(expr: *const SymclawExpr) -> *mut c_char {
|
||||
match unsafe { borrow_expr(expr) } {
|
||||
Some(e) => string_to_c(latex::to_latex(e)),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Operations ──────────────────────────────────────────────────
|
||||
|
||||
/// Simplify an expression. Returns `NULL` on null input.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_simplify(expr: *const SymclawExpr) -> *mut SymclawExpr {
|
||||
match unsafe { borrow_expr(expr) } {
|
||||
Some(e) => {
|
||||
let simplified = simplify::simplify(e);
|
||||
box_expr((*simplified).clone())
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Differentiate an expression with respect to a variable.
|
||||
/// Returns `NULL` on null input.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_differentiate(
|
||||
expr: *const SymclawExpr,
|
||||
var: *const c_char,
|
||||
) -> *mut SymclawExpr {
|
||||
let e = match unsafe { borrow_expr(expr) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let v = match unsafe { cstr_to_str(var) } {
|
||||
Some(v) => v,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let sym = Symbol::new(v);
|
||||
let result = differentiate::differentiate(e, sym);
|
||||
box_expr((*result).clone())
|
||||
}
|
||||
|
||||
/// Integrate an expression with respect to a variable (indefinite).
|
||||
/// Returns `NULL` on null input or if integration fails.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_integrate(
|
||||
expr: *const SymclawExpr,
|
||||
var: *const c_char,
|
||||
) -> *mut SymclawExpr {
|
||||
let e = match unsafe { borrow_expr(expr) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let v = match unsafe { cstr_to_str(var) } {
|
||||
Some(v) => v,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let sym = Symbol::new(v);
|
||||
match integrate::integrate(e, sym) {
|
||||
Some(result) => box_expr((*result).clone()),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate an expression numerically with one variable binding.
|
||||
/// Returns `NaN` on error.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_eval(expr: *const SymclawExpr, var: *const c_char, value: f64) -> f64 {
|
||||
let e = match unsafe { borrow_expr(expr) } {
|
||||
Some(e) => e,
|
||||
None => return f64::NAN,
|
||||
};
|
||||
let v = match unsafe { cstr_to_str(var) } {
|
||||
Some(v) => v,
|
||||
None => return f64::NAN,
|
||||
};
|
||||
let sym = Symbol::new(v);
|
||||
let mut vars = std::collections::HashMap::new();
|
||||
vars.insert(sym, value);
|
||||
eval::eval(e, &vars).unwrap_or(f64::NAN)
|
||||
}
|
||||
|
||||
/// Expand an expression (currently simplifies; full expansion TBD).
|
||||
/// Returns `NULL` on null input.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_expand(expr: *const SymclawExpr) -> *mut SymclawExpr {
|
||||
// No dedicated expand yet; use simplify as a stand-in.
|
||||
symclaw_simplify(expr)
|
||||
}
|
||||
|
||||
/// Solve `expr = 0` for `var`. Returns a JSON array of solution strings.
|
||||
/// Caller must free the returned string with `symclaw_free_string`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_solve(expr: *const SymclawExpr, var: *const c_char) -> *mut c_char {
|
||||
let e = match unsafe { borrow_expr(expr) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let v = match unsafe { cstr_to_str(var) } {
|
||||
Some(v) => v,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let sym = Symbol::new(v);
|
||||
let solutions = solve::solve(e, sym);
|
||||
let strs: Vec<String> = solutions.iter().map(|s| format!("{s}")).collect();
|
||||
let json = format!(
|
||||
"[{}]",
|
||||
strs.iter()
|
||||
.map(|s| format!("\"{s}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
);
|
||||
string_to_c(json)
|
||||
}
|
||||
|
||||
/// Taylor series expansion about `point` to given `order`.
|
||||
/// Returns `NULL` on null input.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_series(
|
||||
expr: *const SymclawExpr,
|
||||
var: *const c_char,
|
||||
point: f64,
|
||||
order: u32,
|
||||
) -> *mut SymclawExpr {
|
||||
let e = match unsafe { borrow_expr(expr) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let v = match unsafe { cstr_to_str(var) } {
|
||||
Some(v) => v,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let sym = Symbol::new(v);
|
||||
let pt = Expr::Float(ordered_float::OrderedFloat(point));
|
||||
let result = series::taylor(e, sym, &pt, order);
|
||||
box_expr((*result).clone())
|
||||
}
|
||||
|
||||
/// Compute the limit of `expr` as `var → point`.
|
||||
/// Returns `NULL` on null input or if the limit does not exist.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_limit(
|
||||
expr: *const SymclawExpr,
|
||||
var: *const c_char,
|
||||
point: f64,
|
||||
) -> *mut SymclawExpr {
|
||||
let e = match unsafe { borrow_expr(expr) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let v = match unsafe { cstr_to_str(var) } {
|
||||
Some(v) => v,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let result = limits::limit_at(e, v, point);
|
||||
match result {
|
||||
limits::LimitResult::Finite(expr) => box_expr(expr),
|
||||
_ => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Arithmetic ──────────────────────────────────────────────────
|
||||
|
||||
/// Add two expressions: a + b.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_add(a: *const SymclawExpr, b: *const SymclawExpr) -> *mut SymclawExpr {
|
||||
let ea = match unsafe { borrow_expr(a) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let eb = match unsafe { borrow_expr(b) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let result = Expr::Add(vec![
|
||||
std::sync::Arc::new(ea.clone()),
|
||||
std::sync::Arc::new(eb.clone()),
|
||||
]);
|
||||
box_expr(result)
|
||||
}
|
||||
|
||||
/// Multiply two expressions: a * b.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_mul(a: *const SymclawExpr, b: *const SymclawExpr) -> *mut SymclawExpr {
|
||||
let ea = match unsafe { borrow_expr(a) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let eb = match unsafe { borrow_expr(b) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let result = Expr::Mul(vec![
|
||||
std::sync::Arc::new(ea.clone()),
|
||||
std::sync::Arc::new(eb.clone()),
|
||||
]);
|
||||
box_expr(result)
|
||||
}
|
||||
|
||||
/// Raise base to exp: base^exp.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_pow(
|
||||
base: *const SymclawExpr,
|
||||
exp: *const SymclawExpr,
|
||||
) -> *mut SymclawExpr {
|
||||
let eb = match unsafe { borrow_expr(base) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let ee = match unsafe { borrow_expr(exp) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let result = Expr::Pow(
|
||||
std::sync::Arc::new(eb.clone()),
|
||||
std::sync::Arc::new(ee.clone()),
|
||||
);
|
||||
box_expr(result)
|
||||
}
|
||||
|
||||
// ── Code Generation ─────────────────────────────────────────────
|
||||
|
||||
/// Generate code in the specified language ("python", "c", "rust", "julia", "javascript").
|
||||
/// Returns `NULL` on null input or unrecognized language.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn symclaw_to_code(
|
||||
expr: *const SymclawExpr,
|
||||
language: *const c_char,
|
||||
) -> *mut c_char {
|
||||
let e = match unsafe { borrow_expr(expr) } {
|
||||
Some(e) => e,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let lang_str = match unsafe { cstr_to_str(language) } {
|
||||
Some(s) => s,
|
||||
None => return ptr::null_mut(),
|
||||
};
|
||||
let lang = match lang_str.to_lowercase().as_str() {
|
||||
"python" => codegen::Language::Python,
|
||||
"c" => codegen::Language::C,
|
||||
"rust" => codegen::Language::Rust,
|
||||
"julia" => codegen::Language::Julia,
|
||||
"javascript" | "js" => codegen::Language::JavaScript,
|
||||
"glsl" => codegen::Language::GLSL,
|
||||
"wgsl" => codegen::Language::WGSL,
|
||||
_ => return ptr::null_mut(),
|
||||
};
|
||||
let opts = codegen::CodegenOptions::new(lang);
|
||||
let code = codegen::generate(e, &opts);
|
||||
string_to_c(code)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Tests
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::CString;
|
||||
|
||||
fn c(s: &str) -> *const c_char {
|
||||
CString::new(s).unwrap().into_raw() as *const c_char
|
||||
}
|
||||
|
||||
unsafe fn read_c_str(p: *mut c_char) -> String {
|
||||
assert!(!p.is_null());
|
||||
let s = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned();
|
||||
symclaw_free_string(p);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_to_string_roundtrip() {
|
||||
let input = c("x + 1");
|
||||
let expr = symclaw_parse(input);
|
||||
assert!(!expr.is_null());
|
||||
let s = symclaw_to_string(expr);
|
||||
let text = unsafe { read_c_str(s) };
|
||||
assert!(!text.is_empty());
|
||||
symclaw_free_expr(expr);
|
||||
// clean up input CString
|
||||
drop(unsafe { CString::from_raw(input as *mut c_char) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn differentiate_x_squared() {
|
||||
let input = c("x^2");
|
||||
let var = c("x");
|
||||
let expr = symclaw_parse(input);
|
||||
let deriv = symclaw_differentiate(expr, var);
|
||||
assert!(!deriv.is_null());
|
||||
let s = symclaw_to_string(deriv);
|
||||
let text = unsafe { read_c_str(s) };
|
||||
// Should contain "2" and "x"
|
||||
assert!(text.contains('2') || text.contains('x'), "got: {text}");
|
||||
symclaw_free_expr(deriv);
|
||||
symclaw_free_expr(expr);
|
||||
drop(unsafe { CString::from_raw(input as *mut c_char) });
|
||||
drop(unsafe { CString::from_raw(var as *mut c_char) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simplify_works() {
|
||||
let input = c("x + 0");
|
||||
let expr = symclaw_parse(input);
|
||||
let simplified = symclaw_simplify(expr);
|
||||
assert!(!simplified.is_null());
|
||||
symclaw_free_expr(simplified);
|
||||
symclaw_free_expr(expr);
|
||||
drop(unsafe { CString::from_raw(input as *mut c_char) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_works() {
|
||||
let input = c("x^2");
|
||||
let var = c("x");
|
||||
let expr = symclaw_parse(input);
|
||||
let result = symclaw_eval(expr, var, 3.0);
|
||||
assert!((result - 9.0).abs() < 1e-10);
|
||||
symclaw_free_expr(expr);
|
||||
drop(unsafe { CString::from_raw(input as *mut c_char) });
|
||||
drop(unsafe { CString::from_raw(var as *mut c_char) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_pointer_safety() {
|
||||
assert!(symclaw_parse(ptr::null()).is_null());
|
||||
assert!(symclaw_to_string(ptr::null()).is_null());
|
||||
assert!(symclaw_differentiate(ptr::null(), ptr::null()).is_null());
|
||||
assert!(symclaw_simplify(ptr::null()).is_null());
|
||||
assert!(symclaw_eval(ptr::null(), ptr::null(), 0.0).is_nan());
|
||||
symclaw_free_expr(ptr::null_mut()); // should not crash
|
||||
symclaw_free_string(ptr::null_mut()); // should not crash
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_without_crash() {
|
||||
let input = c("42");
|
||||
let expr = symclaw_parse(input);
|
||||
symclaw_free_expr(expr);
|
||||
// no crash = pass
|
||||
drop(unsafe { CString::from_raw(input as *mut c_char) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latex_output() {
|
||||
let input = c("x^2");
|
||||
let expr = symclaw_parse(input);
|
||||
let s = symclaw_to_latex(expr);
|
||||
let text = unsafe { read_c_str(s) };
|
||||
assert!(!text.is_empty());
|
||||
symclaw_free_expr(expr);
|
||||
drop(unsafe { CString::from_raw(input as *mut c_char) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_generation() {
|
||||
let input = c("x^2");
|
||||
let lang = c("python");
|
||||
let expr = symclaw_parse(input);
|
||||
let code = symclaw_to_code(expr, lang);
|
||||
let text = unsafe { read_c_str(code) };
|
||||
assert!(!text.is_empty());
|
||||
symclaw_free_expr(expr);
|
||||
drop(unsafe { CString::from_raw(input as *mut c_char) });
|
||||
drop(unsafe { CString::from_raw(lang as *mut c_char) });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user