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:
redclawsystems
2026-05-19 04:39:49 +00:00
commit f4b75db2ee
291 changed files with 130230 additions and 0 deletions
+486
View File
@@ -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) });
}
}