Skip to content
Closed
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
101 changes: 101 additions & 0 deletions MathildaXeus.command
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/bin/sh
# MathildaXeus.command — double-click to open Mathilda in a Jupyter (xeus) front end.
#
# It lives at the REPOSITORY ROOT, beside MathildaNotebook.command, and resolves
# everything from its own location, so it works however it is invoked and
# wherever the repo is checked out.
#
# WHY THE .command SUFFIX. It is what makes the file clickable: Finder runs a
# .command file in Terminal (a plain executable opens in a text editor instead),
# and hides the extension, so it reads as "MathildaXeus" in the folder.
#
# WHAT IT LAUNCHES. The xeus-based Jupyter kernel (kernel/, built as `xmathilda`)
# running under JupyterLab. Unlike the Tauri desktop notebook (MathildaNotebook),
# this is the standard Jupyter ecosystem front end — useful for users who already
# live in JupyterLab / VS Code / Colab. See kernel/README.md.
#
# WHERE THE TOOLCHAIN COMES FROM. The kernel needs the xeus toolchain + Jupyter,
# which are not vendored. Point this at a conda-forge env (recommended) via any
# of, in order: $MATHILDA_XEUS_ENV, an active $CONDA_PREFIX, or a conda/mamba/
# micromamba env named `mathilda-xeus`. Create one once with:
#
# micromamba create -n mathilda-xeus -c conda-forge \
# xeus xeus-zmq cppzmq nlohmann_json xtl cmake jupyterlab jupyter_client
#
# This script then builds + installs the kernel into that env if needed and
# opens JupyterLab with the Mathilda kernel available.

set -e

ROOT=$(cd "$(dirname "$0")" && pwd)

if [ "$(uname -s)" != "Darwin" ]; then
echo "This launcher is macOS-only (it opens a Terminal via Finder)."
echo "On Linux, activate your xeus env and run:"
echo " cmake -S '$ROOT/kernel' -B '$ROOT/kernel/build' -DCMAKE_PREFIX_PATH=\"\$CONDA_PREFIX\""
echo " cmake --build '$ROOT/kernel/build' -j && cmake --install '$ROOT/kernel/build' --prefix \"\$CONDA_PREFIX\""
echo " jupyter lab"
exit 1
fi

PATH="$HOME/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:/opt/local/bin:$PATH"
export PATH

# --- Locate the xeus / Jupyter environment ---------------------------------
ENV_PREFIX=""
if [ -n "$MATHILDA_XEUS_ENV" ] && [ -x "$MATHILDA_XEUS_ENV/bin/jupyter" ]; then
ENV_PREFIX="$MATHILDA_XEUS_ENV"
elif [ -n "$CONDA_PREFIX" ] && [ -x "$CONDA_PREFIX/bin/jupyter" ]; then
ENV_PREFIX="$CONDA_PREFIX"
else
for mgr in micromamba mamba conda; do
if command -v "$mgr" >/dev/null 2>&1; then
# `<mgr> run -n mathilda-xeus` resolves the env without needing its
# shell hook to be initialised in this non-interactive Terminal.
CAND=$("$mgr" run -n mathilda-xeus printenv CONDA_PREFIX 2>/dev/null || true)
if [ -n "$CAND" ] && [ -x "$CAND/bin/jupyter" ]; then
ENV_PREFIX="$CAND"
break
fi
fi
done
fi

if [ -z "$ENV_PREFIX" ]; then
echo "No xeus/Jupyter environment found."
echo
echo "Create one once (conda-forge has the whole toolchain, no Python kernel"
echo "dependency at run time):"
echo
echo " micromamba create -n mathilda-xeus -c conda-forge \\"
echo " xeus xeus-zmq cppzmq nlohmann_json xtl cmake jupyterlab jupyter_client"
echo
echo "then double-click this file again (or set MATHILDA_XEUS_ENV to its path)."
exit 1
fi

JUPYTER="$ENV_PREFIX/bin/jupyter"
CMAKE="$ENV_PREFIX/bin/cmake"
[ -x "$CMAKE" ] || CMAKE=cmake
echo "Using environment: $ENV_PREFIX"

# --- Build + install the kernel if its kernelspec is missing ---------------
# The kernelspec, not the binary, is the source of truth: `jupyter` launches the
# kernel through it. (Re)build when absent; the Mathilda makefile builds
# libmathilda.a with its own GCC toolchain, independent of this env's compiler.
if ! JUPYTER_PATH="$ENV_PREFIX/share/jupyter" "$JUPYTER" kernelspec list 2>/dev/null | grep -q "xmathilda"; then
echo "The Mathilda (xmathilda) kernel is not installed in this environment."
echo "Building and installing it now — the first build takes a few minutes."
echo
"$CMAKE" -S "$ROOT/kernel" -B "$ROOT/kernel/build" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$ENV_PREFIX" \
-DCMAKE_INSTALL_PREFIX="$ENV_PREFIX" \
-DCMAKE_INSTALL_RPATH="$ENV_PREFIX/lib"
"$CMAKE" --build "$ROOT/kernel/build" -j
"$CMAKE" --install "$ROOT/kernel/build"
echo
fi

echo "Opening JupyterLab — pick the \"Mathilda\" kernel for a new notebook."
exec "$JUPYTER" lab
1 change: 1 addition & 0 deletions kernel/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
99 changes: 99 additions & 0 deletions kernel/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#############################################################################
# Mathilda xeus Jupyter kernel (xmathilda)
#
# Builds a native C++17 Jupyter kernel that embeds Mathilda's evaluator in
# process via its C ABI (src/ffi/mathilda_ffi.h). No Python is needed at
# runtime — only xeus and the Mathilda static library.
#
# Quick start (with the xeus toolchain from conda-forge on PATH / CMAKE_PREFIX_PATH):
# cmake -S kernel -B kernel/build -DCMAKE_BUILD_TYPE=Release
# cmake --build kernel/build -j
# cmake --install kernel/build --prefix <env> # installs the kernelspec
#############################################################################
cmake_minimum_required(VERSION 3.16)
project(xmathilda VERSION 0.1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()

# --- The Mathilda source tree (one level up by default) --------------------
get_filename_component(MATHILDA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
set(MATHILDA_ROOT "${MATHILDA_ROOT}" CACHE PATH "Path to the Mathilda source tree")

# Extra `make` flags used when (re)building libmathilda.a, e.g. to drop
# optional dependencies for a leaner, headless kernel:
# -DMATHILDA_MAKE_FLAGS="USE_GRAPHICS=0;USE_FFTW=0;USE_ECM=0"
set(MATHILDA_MAKE_FLAGS "" CACHE STRING "Extra flags passed to `make libmathilda.a`")

# --- xeus toolchain --------------------------------------------------------
find_package(xeus REQUIRED)
find_package(xeus-zmq REQUIRED)
find_package(nlohmann_json REQUIRED)
find_package(Threads REQUIRED)

# --- Build (or locate) libmathilda.a ---------------------------------------
# The archive is the whole evaluator minus main() (repl.o), plus the FFI entry
# points. Built by the Mathilda makefile with its own GCC toolchain — which is
# independent of this CMake project's C++ compiler (they meet only at the C
# ABI, which is portable).
set(MATHILDA_LIB "${MATHILDA_ROOT}/libmathilda.a")
add_custom_command(
OUTPUT "${MATHILDA_LIB}"
COMMAND ${CMAKE_COMMAND} -E echo "Building libmathilda.a via the Mathilda makefile..."
COMMAND make -C "${MATHILDA_ROOT}" libmathilda.a ${MATHILDA_MAKE_FLAGS}
WORKING_DIRECTORY "${MATHILDA_ROOT}"
COMMENT "make libmathilda.a"
VERBATIM)
add_custom_target(mathilda_lib DEPENDS "${MATHILDA_LIB}")

# --- Transitive C dependencies of libmathilda.a ----------------------------
# Which of these are actually referenced depends on the USE_* flags the archive
# was built with. find_library is tolerant: a missing optional lib is simply
# not linked. Override MATHILDA_EXTRA_LIBS to pin an exact set.
set(_mth_libs "")
foreach(_name gmp mpfr flint pcre2-8 fftw3 ecm readline raylib)
find_library(_lib_${_name} NAMES ${_name})
if(_lib_${_name})
list(APPEND _mth_libs ${_lib_${_name}})
endif()
endforeach()
if(APPLE)
# LAPACK/BLAS via Accelerate; CoreGraphics/CoreFoundation for the raylib
# offscreen path. On Linux use -DMATHILDA_EXTRA_LIBS="lapacke;lapack;blas;gfortran".
list(APPEND _mth_libs "-framework Accelerate"
"-framework CoreGraphics"
"-framework CoreFoundation")
endif()
set(MATHILDA_EXTRA_LIBS "${_mth_libs}" CACHE STRING "Transitive libs for libmathilda.a")

# --- The kernel executable -------------------------------------------------
add_executable(xmathilda
src/main.cpp
src/mathilda_interpreter.cpp)
add_dependencies(xmathilda mathilda_lib)

target_include_directories(xmathilda PRIVATE
"${MATHILDA_ROOT}/src"
"${MATHILDA_ROOT}/src/ffi")

target_link_libraries(xmathilda PRIVATE
xeus xeus-zmq nlohmann_json::nlohmann_json
"${MATHILDA_LIB}"
${MATHILDA_EXTRA_LIBS}
Threads::Threads
m)

# --- Kernelspec installation ----------------------------------------------
# kernel.json tells Jupyter how to launch the kernel; MATHILDA_HOME points the
# loader at the internal/ module tree (init.m etc.) shipped in the source.
set(XEUS_MATHILDA_HOME "${MATHILDA_ROOT}/src" CACHE PATH "Dir containing internal/")
set(XKERNEL_BIN "${CMAKE_INSTALL_PREFIX}/bin/xmathilda")
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/kernel.json.in"
"${CMAKE_CURRENT_BINARY_DIR}/kernel.json" @ONLY)

install(TARGETS xmathilda RUNTIME DESTINATION bin)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/kernel.json"
DESTINATION share/jupyter/kernels/xmathilda)
73 changes: 73 additions & 0 deletions kernel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# xmathilda — a xeus-based Jupyter kernel for Mathilda

A native C++ Jupyter kernel that runs Mathilda in process. It embeds the
evaluator through Mathilda's C ABI (`src/ffi/mathilda_ffi.h`) and lets
[xeus](https://github.com/jupyter-xeus/xeus) handle the Jupyter wire protocol,
so there is **no Python dependency at run time** — only the Jupyter *front end*
(Lab, Notebook, `nbclient`, VS Code, …) is Python, and it is not this kernel's
concern.

This is an interop on-ramp to the standard Jupyter ecosystem; it complements,
rather than replaces, the Tauri desktop notebook in [`../frontend`](../frontend).

## What it supports

| Jupyter request | Mathilda behaviour |
|---|---|
| `execute_request` | A cell may hold several statements (split like a Mathematica input cell). `Print` → stdout stream; `Head::tag` warnings → stderr; a `;`- or `Null`-valued statement shows nothing; results carry `text/latex` (KaTeX) + `text/plain`. The last result is the `execute_result` (`Out[n]`), earlier ones are `display_data`. `%`, `%%`, `Out[n]` work across cells. |
| `Plot[…]` / `Graphics[…]` | `application/vnd.plotly.v1+json` (renders in JupyterLab/nbviewer with the Plotly renderer). |
| `complete_request` | Tab completion over defined symbol names (prefix match). |
| `inspect_request` | `Shift-Tab` / `?name` docstring. |
| `is_complete_request` | Console continuation (balanced brackets / strings / comments). |

Follow-ups: `image/png` for `Image[…]` (currently `application/json` of raw
RGBA), a self-contained HTML+plotly.js fallback for front ends without the
Plotly renderer, and true mid-computation interrupt (needs an abort flag in the
evaluator loop).

## Build

The kernel needs the xeus toolchain (xeus ≥ 5, xeus-zmq ≥ 3, `nlohmann_json`).
The simplest source is conda-forge:

```bash
micromamba create -n mathilda-xeus -c conda-forge \
xeus xeus-zmq cppzmq nlohmann_json xtl cmake jupyter_client
micromamba activate mathilda-xeus
```

Then, from the repository root:

```bash
cmake -S kernel -B kernel/build -DCMAKE_BUILD_TYPE=Release
cmake --build kernel/build -j
cmake --install kernel/build --prefix "$CONDA_PREFIX" # installs the kernelspec
```

`libmathilda.a` is built automatically by the Mathilda makefile (with its own
GCC toolchain — it meets the C++ kernel only at the portable C ABI). For a
leaner, headless kernel, drop optional dependencies:

```bash
cmake -S kernel -B kernel/build \
-DMATHILDA_MAKE_FLAGS="USE_GRAPHICS=0;USE_FFTW=0;USE_ECM=0"
```

Plotly output still works with `USE_GRAPHICS=0` (the `Graphics → Plotly JSON`
serializer is compiled regardless; only the on-screen/GL raster paths drop).

On Linux, if `libmathilda.a` was built against reference LAPACK, pass its link
libraries: `-DMATHILDA_EXTRA_LIBS="lapacke;lapack;blas;gfortran"`.

## Use

```bash
jupyter kernelspec list # shows "mathilda"
jupyter console --kernel xmathilda # or open a notebook and pick the Mathilda kernel
```

```
In[1]:= Integrate[x^2, x] (* typeset result *)
In[2]:= Table[Prime[k], {k, 5}]
In[3]:= Plot[Sin[x], {x, 0, 2 Pi}] (* Plotly figure *)
```
12 changes: 12 additions & 0 deletions kernel/kernel.json.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"display_name": "Mathilda",
"argv": [
"@XKERNEL_BIN@",
"-f",
"{connection_file}"
],
"language": "mathilda",
"env": {
"MATHILDA_HOME": "@XEUS_MATHILDA_HOME@"
}
}
37 changes: 37 additions & 0 deletions kernel/src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// main.cpp — entry point for the Mathilda xeus kernel (xmathilda).
//
// Boilerplate from the xeus kernel-authoring guide: load the connection file
// Jupyter passes as `-f <file>`, create a ZMQ context and the interpreter,
// then run the kernel. xeus owns the Jupyter wire protocol; all CAS behaviour
// is in mathilda_kernel::interpreter.

#include <memory>
#include <string>

#include <xeus/xkernel.hpp>
#include <xeus/xkernel_configuration.hpp>
#include <xeus/xhelper.hpp>

#include <xeus-zmq/xzmq_context.hpp>
#include <xeus-zmq/xserver_zmq.hpp>

#include "mathilda_interpreter.hpp"

int main(int argc, char* argv[])
{
// `-f <connection_file>` — Jupyter's launch convention (removed from argv).
const std::string connection_filename = xeus::extract_filename(argc, argv);

auto context = xeus::make_zmq_context();
auto interpreter = std::make_unique<mathilda_kernel::interpreter>();

xeus::xconfiguration config = xeus::load_configuration(connection_filename);

xeus::xkernel kernel(config,
xeus::get_user_name(),
std::move(context),
std::move(interpreter),
xeus::make_xserver_default);
kernel.start();
return 0;
}
Loading
Loading