Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ C++ wrapper for [GIAC](https://xcas.univ-grenoble-alpes.fr/) computer algebra sy

- String-based expression evaluation
- Context management for variable persistence
- Per-context evaluation via `giac_eval(expr, ctx)` so distinct `GiacContext` instances isolate `:=` bindings ([#3](https://github.com/s-celles/libgiac-julia-wrapper/issues/3))
- Native Gen object manipulation
- Support for Linux, macOS, and Windows

Expand Down
9 changes: 8 additions & 1 deletion meson.build
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
project('giac_wrapper', 'cpp',
version: '0.5.0',
version: '0.6.0',
meson_version: '>= 1.2.0',
license: 'GPL-3.0-or-later',
default_options: [
Expand All @@ -9,6 +9,13 @@ project('giac_wrapper', 'cpp',
],
)

# Expose the project version to C++ so get_wrapper_version() never falls
# out of sync with meson.build (issue #2).
add_project_arguments(
'-DWRAPPER_VERSION="' + meson.project_version() + '"',
language: 'cpp',
)

# Find dependencies
# JlCxx (libcxxwrap-julia) ships CMake config files, use Meson's cmake module
jlcxx_dep = dependency('JlCxx',
Expand Down
18 changes: 17 additions & 1 deletion src/giac_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ std::string get_giac_version() {
}

std::string get_wrapper_version() {
return "0.1.0";
#ifdef WRAPPER_VERSION
return WRAPPER_VERSION;
#else
return "unknown";
#endif
}

bool check_giac_available() {
Expand Down Expand Up @@ -161,6 +165,18 @@ Gen giac_eval(const std::string& expr) {
return Gen(std::make_unique<GenImpl>(result));
}

Gen giac_eval(const std::string& expr, GiacContext& ctx) {
initialize_giac_library();
giac::context* gctx = ctx.impl_->ctx;
try {
giac::gen parsed = giac::gen(expr, gctx);
giac::gen result = giac::eval(parsed, gctx);
return Gen(std::make_unique<GenImpl>(result));
} catch (const std::exception& e) {
throw std::runtime_error(std::string("GIAC evaluation error: ") + e.what());
}
}

// ============================================================================
// Generic Dispatch Implementation
// ============================================================================
Expand Down
19 changes: 18 additions & 1 deletion src/giac_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ namespace giac_julia {
// Forward declaration of opaque types
struct GiacContextImpl;
struct GenImpl;
class Gen; // Forward declaration for free functions
class Gen; // Forward declaration for free functions
class GiacContext; // Forward declaration for free functions taking a context

// ============================================================================
// Version Functions
Expand Down Expand Up @@ -49,9 +50,21 @@ int help_count();
* @param expr Expression string (e.g., "sin(x)+1", "[[1,2],[3,4]]")
* @return Evaluated Gen
* @note This is the preferred entry point for string expressions
* @note Uses a process-wide singleton context; use the (expr, ctx)
* overload below for per-context isolation.
*/
Gen giac_eval(const std::string& expr);

/**
* @brief Parse and evaluate a Giac expression string in a specific context
* @param expr Expression string
* @param ctx Context whose state (variable bindings, configuration) is
* used and updated by this evaluation. Two distinct GiacContext
* instances do not share `:=` bindings, enabling true isolation.
* @return Evaluated Gen
*/
Gen giac_eval(const std::string& expr, GiacContext& ctx);

// ============================================================================
// Generic Dispatch (Tier 2)
// ============================================================================
Expand Down Expand Up @@ -291,6 +304,9 @@ class GiacContext {

private:
std::unique_ptr<GiacContextImpl> impl_;

// Free function that needs access to the underlying giac::context*.
friend Gen giac_eval(const std::string& expr, GiacContext& ctx);
};

// ============================================================================
Expand Down Expand Up @@ -403,6 +419,7 @@ class Gen {

// Friend functions that need access to private constructor
friend Gen giac_eval(const std::string& expr);
friend Gen giac_eval(const std::string& expr, GiacContext& ctx);
friend Gen apply_func0(const std::string& name);
friend Gen apply_func1(const std::string& name, const Gen& arg);
friend Gen apply_func2(const std::string& name, const Gen& arg1, const Gen& arg2);
Expand Down
9 changes: 7 additions & 2 deletions src/giac_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,13 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& mod)
.method("expand", &Gen::expand)
.method("factor", &Gen::factor);

// Register giac_eval free function
mod.method("giac_eval", &giac_eval);
// Register giac_eval free function (both overloads).
// The (expr) overload uses the singleton thread-local context; the
// (expr, ctx) overload provides per-context isolation (issue #3).
mod.method("giac_eval",
static_cast<Gen(*)(const std::string&)>(&giac_eval));
mod.method("giac_eval",
static_cast<Gen(*)(const std::string&, GiacContext&)>(&giac_eval));

// Register generic dispatch functions (Tier 2)
mod.method("apply_func0", &apply_func0);
Expand Down
55 changes: 55 additions & 0 deletions tests/cpp/test_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,59 @@ TEST(context_isolation) {
ASSERT_EQ("20", ctx2.get_variable("x"));
}

// Issue #3: free-function giac_eval(expr, ctx) — returns a Gen and
// preserves per-context isolation. Binding `a := 5` through one context
// must NOT be visible to a fresh context.
TEST(giac_eval_with_context_returns_gen) {
GiacContext ctx1;
GiacContext ctx2;

// Bind in ctx1 via the new context-aware giac_eval.
Gen r1 = giac_eval("ctx_iso_a := 7", ctx1);
ASSERT_EQ("7", r1.to_string());

// Reading the same name in ctx1 sees the binding.
Gen r1_read = giac_eval("ctx_iso_a", ctx1);
ASSERT_EQ("7", r1_read.to_string());

// ctx2 is independent — the same name is still the unbound symbol.
Gen r2_read = giac_eval("ctx_iso_a", ctx2);
ASSERT_EQ("ctx_iso_a", r2_read.to_string());
}

// Issue #3 regression (MCP scenario): binding `y` in one context must not
// poison `desolve(..., y)` in a fresh, independent context. Under the
// pre-fix singleton-context behavior, this combination produced
// `Error: Dependent variable assigned. Run purge(y)`.
TEST(issue3_bound_var_does_not_poison_desolve_in_other_context) {
GiacContext ctx_with_binding;
GiacContext ctx_fresh;

// Establish the binding that used to leak.
(void) giac_eval("y := 42", ctx_with_binding);

// In a fresh, independent context, a desolve referencing y must work.
// (Calling this same desolve in ctx_with_binding throws
// "Dependent variable assigned. Run purge(y)" — verified manually.)
Gen result = giac_eval("desolve(diff(y,t)=cos(t), t, y)", ctx_fresh);
std::string s = result.to_string();

if (s.find("Dependent variable") != std::string::npos) {
throw std::runtime_error(
"desolve in ctx_fresh was poisoned by ctx_with_binding's binding: "
+ s
);
}
if (s.empty()) {
throw std::runtime_error("desolve returned an empty result string");
}

// And ctx_with_binding still holds y = 42 — bindings inside a context
// persist across calls within that context.
Gen y_in_ctx1 = giac_eval("y", ctx_with_binding);
ASSERT_EQ("42", y_in_ctx1.to_string());
}

// Test timeout configuration
TEST(timeout_config) {
GiacContext ctx;
Expand Down Expand Up @@ -84,6 +137,8 @@ int main() {

RUN_TEST(variable_assignment);
RUN_TEST(context_isolation);
RUN_TEST(giac_eval_with_context_returns_gen);
RUN_TEST(issue3_bound_var_does_not_poison_desolve_in_other_context);
RUN_TEST(timeout_config);
RUN_TEST(precision_config);
RUN_TEST(complex_mode);
Expand Down
7 changes: 6 additions & 1 deletion tests/cpp/test_eval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ TEST(error_handling) {
TEST(version_functions) {
std::string gv = get_giac_version();
std::string wv = get_wrapper_version();
ASSERT_EQ("0.1.0", wv);
// Issue #2: wrapper_version() now comes from meson.project_version()
// at build time. Check it is non-empty and SemVer-shaped (X.Y.Z...)
// rather than pinning a literal.
assert(!wv.empty());
assert(wv != "unknown");
assert(wv.find('.') != std::string::npos);
// GIAC version should be non-empty
assert(!gv.empty());
}
Expand Down
17 changes: 17 additions & 0 deletions tests/julia/test_context.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ end
@test get_variable(ctx2, "x") == "20"
end

# Issue #3: the new free-function `giac_eval(expr, ctx)` returns a Gen
# and preserves per-context isolation of `:=` bindings.
@testset "giac_eval(expr, ctx) — returns Gen and isolates bindings" begin
ctx1 = GiacContext()
ctx2 = GiacContext()

# Bind via the new overload, exercised from Julia.
r1 = giac_eval("ctx_iso_b := 11", ctx1)
@test to_string(r1) == "11"

# ctx1 sees the binding.
@test to_string(giac_eval("ctx_iso_b", ctx1)) == "11"

# ctx2 is independent — still the unbound symbol.
@test to_string(giac_eval("ctx_iso_b", ctx2)) == "ctx_iso_b"
end

@testset "Timeout Configuration" begin
ctx = GiacContext()

Expand Down
5 changes: 4 additions & 1 deletion tests/julia/test_eval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ end
@testset "Version Functions" begin
@test giac_version() isa String
@test !isempty(giac_version())
@test wrapper_version() == "0.1.0"
# Issue #2: wrapper_version() now reads from meson.project_version()
# at build time, so it never falls out of sync with meson.build.
# Accept any SemVer-shaped string instead of a hardcoded literal.
@test occursin(r"^\d+\.\d+\.\d+", wrapper_version())
end

@testset "GIAC Availability" begin
Expand Down
Loading