The fastest pure-Java math runtime, now with GPU (CUDA, Metal and OpenCL) bulk evaluators and a fully open-sourced Vector API (SIMD) kernel. Zero JNI. Zero native binaries. Zero bytecode-safety risk.
ParserNG is an ultra-high-performance mathematical runtime built for modern JVM workloads: real-time plotting pipelines, financial modeling, and deep learning activation functions (SwiGLU, GELU, and the rest of the Transformer toolkit). By adopting a hardware-aligned, fast-interpreted memory model instead of risky dynamic bytecode generation, ParserNG eliminates classloader bloat, protects your runtime from native segmentation faults, and dramatically simplifies your Software Bill of Materials (SBOM) compliance posture.
Five execution tiers, one parser, one syntax, and now big data. Pick the tier that matches how hot your loop is; you never rewrite the expression to move up a tier.
ParserNG powers math visualization, analytical pipelines, and platform tooling at 167 organizations around the globe. Need enterprise-grade confidence? Book a free 30-minute introductory call to discuss your production needs. Ongoing consulting, custom development, and support engagements are available after the initial conversation: Schedule a call.
The full change log for each release is in LATEST.md.
3.1.0: Expands the calculus capabilities by including a partial differential equations engine in pure Java.
3.0.9.3: Incubating OpenCL LLM Engine optimization and bug fixes to MathScanner, RootFinder and Matrix#det
MathExpression, ParserNG's core class, now has a strict flag (an instance field). It decides what happens when an expression uses a variable or method name that has not been declared:
strict = false(the default, so existing behaviour is preserved): an undeclared name is accepted as a variable initialised to zero.strict = true: an undeclared variable or method name throws a parser exception when the expression is constructed.
parser-ng-parquetjoins the ParserNG family. It reads Parquet files, local and remote (for example on S3), at perhaps the highest rates available on the JVM: about 3x to 4x the rate ofparquet-javaitself.parser-ng-sqlnow supportsselectover Parquet files, local and remote.- The base
MathExpressionconstructor is much faster, andBaseNFunction, theMathExpressionclass extended to every number base from 2 to 36 inclusive, is now complete. - A hidden allocation source that occasionally appeared in the SIMD bulk evaluators has been removed.
- The float32 SIMD bulk evaluators (
SIMDCommandF32,SIMDCommandSegmentF32) passed 1.4 billion evaluations per second on the quadraticx^2+2*x+1, using just 2 workers on an Intel mobile Skylake processor (2015 or 2016 vintage) in a Dell Inspiron 5759. - Please read the READMEs of
parser-ng-sqlandparser-ng-parquet, as there are many new updates.
- Version 3.0.5 introduced float32 (Java's
float) and float64 (Java's primitivedouble) bulk evaluators, alongside MemorySegment-based bulk evaluators for both precisions. It also began retiringSIMDEngineEvaluatorandSIMDCommandTurboEvaluatorin favour of theSIMDCommandclasses (SIMDCommandF32,SIMDCommandF64,SIMDCommandSegmentF32andSIMDCommandSegmentF64), which keep the same API. parser-ng-arrowputs all of that to work: a true zero-copy bridge between ParserNG and Apache Arrow. You can evaluate any runtime string expression, filter rows, and project computed columns directly over Arrow's columnar memory, with no serialization, no staging buffers, and no leaving the Arrow buffer at all. In its SIMD mode (ArrowBulkEvaluator), it beats Apache Gandiva by 10 to 30 times on every transcendental function:sin,cos,log,exp, and the rest of the set. Gandiva still edges it out on hardware-intrinsicsqrtand plain arithmetic, by 1.5x to 2x, but turn onArrowBulkEvaluator's built-in parallelism and that gap is gone at just 2 workers.- For workloads that want more than the CPU can give,
ArrowGpuBulkEvaluatordispatches the same expressions straight to CUDA, OpenCL or Metal, with zero JNI and zero native binaries anywhere in your build. parser-ng-sql, introduced in 3.0.7 for users ofparser-ng-arrow, received major speed and bug fixes in 3.0.8.- 3.0.6 added real query power to
parser-ng-arrow:filter,projectandfilterProject(see below). - 3.0.8 brought the CPU-pinned parallel model, the native GPU bulk evaluators and the
ARRAYtype (see the table below).
This is the part of ParserNG worth losing sleep over, so it goes first.
One kernel, three backends, your choice of double or genuinely native float32 (not double silently upcast: a lot of consumer GPUs run fp64 at a fraction of their fp32 rate, so faking float32 would defeat the entire point). ParserNG auto-detects whichever backend is installed, or you pick explicitly:
MathExpression me = new MathExpression("3*cos(x-2)+ln(3*x^3-5*x-4*tan(x))");
VectorTurboEvaluator vte = new VectorTurboEvaluator(me);
try (GpuCompositeExpression gpu = GpuExpressionBridge.from(vte)) { // auto-picks CUDA, falls back to OpenCL/Metal
double[] flat = /* your sample buffer, column-major per variable slot */;
double[] out = new double[flat.length];
gpu.applyBulk(flat, out); // full double precision
// gpu.applyBulk(floatIn, floatOut) // native float32 kernel: a real throughput gain, not a cast
}Multiple GPUs installed? Select by vendor, not by driver-string guesswork:
OpenClCompositeExpression.selectDevice(OpenClCompositeExpression.GpuVendor.AMD);
// or: OpenClCompositeExpression.listAvailableDevices().forEach(System.out::println);MemorySegments allow zero-copy transfers. The segment path and the float[] path give identical results:
import com.github.gbenroscience.gpu.opencl.OpenClCompositeExpression;
import com.github.gbenroscience.gpu.opencl.OpenClExpressionBridge;
import com.github.gbenroscience.parser.MathExpression;
import com.github.gbenroscience.simd.turbo.tools.VectorTurboEvaluator;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
MathExpression me = new MathExpression("2*x^2-3*x+1");
VectorTurboEvaluator vte = new VectorTurboEvaluator(me);
try (OpenClCompositeExpression gpu = (OpenClCompositeExpression) OpenClExpressionBridge.from(vte);
Arena arena = Arena.ofConfined()) {
int dataSize = 2_000_000;
float[] flat = new float[dataSize];
for (int i = 0; i < dataSize; i++) {
flat[i] = (float) (-25.6 + i * 0.1);
}
// float[] path
float[] outViaArray = new float[dataSize];
gpu.applyBulk(flat, outViaArray);
// MemorySegment path (float32)
MemorySegment inSeg = arena.allocate((long) dataSize * ValueLayout.JAVA_FLOAT.byteSize());
MemorySegment outSeg = arena.allocate((long) dataSize * ValueLayout.JAVA_FLOAT.byteSize());
MemorySegment.copy(flat, 0, inSeg, ValueLayout.JAVA_FLOAT, 0, dataSize);
gpu.applyBulkF32(inSeg, outSeg);
// outSeg now holds exactly the values in outViaArray
}The most efficient CPU-bound tier ParserNG has. CPU pinning (best on Linux) means 2 workers on 2 cores get you about 1.8x to 2.0x the throughput of 1 worker on 1 core: real scaling, not the diminishing returns you get from unpinned thread pools fighting the scheduler.
MathExpression me = new MathExpression("3*sin(x)*cos(y)+sqrt(abs(x*y))");
var evaluator = (SIMDCommandF64.SIMDVectorCompositeExpression) new SIMDCommandF64(me, 2).compile(); // 2 pinned workers
double[][] inputs = new double[2][dataSize]; // x, y
double[] out = new double[dataSize];
evaluator.applyBulkParallel(inputs, out);The number of workers is fixed when you create the evaluator. For float32 data use SIMDCommandF32. For greater throughput, SIMDCommandSegmentF64 (and SIMDCommandSegmentF32) work over MemorySegments. Parallel work only happens when you call applyBulkParallel, and the dataset must be large enough to exceed the parallel threshold. Use a shared arena so that the worker threads can all read the segments:
MathExpression me = new MathExpression(
"(1 / (x1 * sqrt(2 * 3.14159))) * exp((-(x2 - x3)^2) / (2 * x1^2))");
var evaluator = SIMDCommandSegmentF64.getEvaluator(me, 2); // 2 workers; omit the argument to use one per CPU
long n = 30_000_000L; // elements per variable
try (Arena arena = Arena.ofShared()) {
MemorySegment in = arena.allocate(3 * n * 8L); // x1, x2, x3: one block of n doubles per variable
MemorySegment out = arena.allocate(n * 8L);
for (long i = 0; i < n; i++) {
in.set(ValueLayout.JAVA_DOUBLE, (0 * n + i) * 8L, 1.5 + i * 0.001); // x1
in.set(ValueLayout.JAVA_DOUBLE, (1 * n + i) * 8L, 2.0 + i * 0.005); // x2
in.set(ValueLayout.JAVA_DOUBLE, (2 * n + i) * 8L, 0.5); // x3
}
evaluator.applyBulkParallel(in, out);
double first = out.get(ValueLayout.JAVA_DOUBLE, 0);
}The four classes SIMDCommandF32, SIMDCommandF64, SIMDCommandSegmentF32 and SIMDCommandSegmentF64 live in parser-ng-gpu-simd and replace SIMDEngineEvaluator and SIMDCommandTurboEvaluator, with the same signatures. The two old names are kept only for backwards compatibility and are deprecated: use the SIMDCommand classes in new code.
Maps your expression straight to 256 or 512-bit AVX lanes via jdk.incubator.vector, instead of hoping the JIT auto-vectorizes:
MathExpression me = new MathExpression("4*x+3*sin(5+x^2)");
var evaluator = (SIMDVectorTurboEvaluator.SIMDVectorCompositeExpression)
new SIMDVectorTurboEvaluator(me).compile();
double[][] inputs = new double[1][dataSize];
double[] out = new double[dataSize];
evaluator.applyBulk(inputs, out);
// evaluator.applyBulkParallel(inputs, out); // works, but JDK 21 has no CPU pinning, so the `SIMDCommand` classes scale betterVectorTurboEvaluator (the non-SIMD-prefixed sibling) takes the opposite bet: no explicit Vector API calls at all, just memory layout and loop shape engineered to coerce the JIT into auto-vectorizing. Use it where the Vector API's incubator status is a concern; use SIMDVectorTurboEvaluator everywhere else.
parser-ng-arrow evaluates ParserNG expressions directly over Apache Arrow batches. These are not toy expressions: they are the kind of thing that runs in production, and the kind an engineering manager cares about because it touches fraud losses, cloud spend or patient safety. They are not a SQL engine, and they are no replacement for Gandiva or DataFusion, but for math-heavy numeric expressions over Arrow batches they are the faster tool for the job. filterProject fuses row selection with column computation, so your projection never runs on a row that is about to be thrown away. When your expressions are float or float64 heavy, parser-ng-arrow is the faster choice, by a lot.
filter, fintech: screening a payments batch for fraud review before a human ever sees it
try (ArrowBulkEvaluator highRisk = ArrowBulkEvaluator.compile("txn_amount > 10000 && risk_score > 0.8")) {
VectorSchemaRoot flagged = highRisk.filter(paymentsBatch);
// Out of a batch that might be millions of transactions wide, "flagged" holds only
// the ones your fraud analysts actually need to look at, computed at SIMD speed.
}project, platform engineering: attaching a real dollar cost to every trace span, at fleet scale
try (ArrowBulkEvaluator costPerRequest =
ArrowBulkEvaluator.compile("(cpu_ms * cpu_rate + mem_gb_s * mem_rate) / request_count")) {
VectorSchemaRoot withCost = costPerRequest.project(traceBatch, "cost_per_request");
// Every span in this minute's trace batch now carries its own "cost_per_request" column,
// computed once over however many million rows your tracing pipeline just handed you:
// the kind of number that ends up on a cloud-spend dashboard your VP actually reads.
}filterProject, healthcare and IoT: two ways to score early-warning risk from a stream of patient vitals
The first is ArrowExpressionEvaluators.filterProject, an ad-hoc, SQL-WHERE/SELECT-shaped convenience. Every expression, predicate included, is plain text compiled fresh on this one call. That is the right shape when the alert rule is something a clinician configured at runtime and is not worth precompiling:
VectorSchemaRoot atRisk = ArrowExpressionEvaluators.filterProject(
vitalsBatch,
"heart_rate > 120 || spo2 < 92",
"patient_id", "heart_rate", "spo2",
"0.4 * (heart_rate - 100) + 0.6 * (94 - spo2)");
// Only the patients whose vitals actually crossed the threshold survive the filter.
// "patient_id", "heart_rate" and "spo2" pass straight through unchanged, zero copy,
// recognized as bare column names rather than routed through the expression engine at all.
// The last one is computed fresh, only over the rows that survived, and comes back
// named after its own trimmed expression text. The result holds exactly these four
// columns, nothing else from vitalsBatch tags along uninvited.The second is the precompiled instance-method form, shown in the filter and project examples above. It is the right shape once the same rule is going to run against every batch streaming off a bedside monitor, not just once:
try (ArrowBulkEvaluator isAbnormal = ArrowBulkEvaluator.compile("heart_rate > 120 || spo2 < 92");
ArrowBulkEvaluator earlyWarningScore =
ArrowBulkEvaluator.compile("0.4 * (heart_rate - 100) + 0.6 * (94 - spo2)")) {
VectorSchemaRoot scored = isAbnormal.filterProject(vitalsBatch, earlyWarningScore, "ews_score");
// Same fusion guarantee (the score never runs on a normal reading), but the predicate
// and the projection are compiled once and reused across every batch this monitor
// streams, instead of being recompiled on every call as in the ad-hoc form above.
// This form also keeps vitalsBatch's full schema and simply appends "ews_score"
// as a new column, rather than returning only the columns you named.
}| Version | What shipped |
|---|---|
| Before 1.0.0 | MathExpression, the interpreter. ParserNG Standard. |
| 1.0.0 to 1.x | Turbo tier arrives: ScalarTurboEvaluator1 (variable args as an array), ScalarTurboEvaluator2 (variable args as widened primitives internally), and MatrixTurboEvaluator, all built on MethodHandles. |
| 2.0.0 to 2.x | Bulk evaluation, via mechanical sympathy and SIMD. VectorTurboEvaluator coerces auto-vectorization through code shape alone; SIMDVectorTurboEvaluator forces it via the explicit Vector API. Both support applyBulkParallel(in, out), but JDK 21 has no CPU pinning, capping the parallel win. |
| 3.0.5 | float32 and float64 bulk evaluators, plus MemorySegment-based bulk evaluators for both precisions. SIMDEngineEvaluator and SIMDCommandTurboEvaluator are retired in favour of the SIMDCommand series: SIMDCommandF32, SIMDCommandF64, SIMDCommandSegmentF32 and SIMDCommandSegmentF64, with the same API. |
| 3.0.6 | filter, project and filterProject in parser-ng-arrow, on both CPU and GPU. |
| 3.0.7 | parser-ng-sql, introduced to help users of parser-ng-arrow, the zero-copy Apache Arrow bridge. |
| 3.0.8 | CPU pinning (best on Linux): 2 workers on 2 cores give about 1.8x to 2.0x the work of 1 worker on 1 core. The pinned evaluators live in parser-ng-gpu-simd (JDK 22+), the module that also houses native GPU bulk evaluators for CUDA, OpenCL and Metal. Also new: the ARRAY type. |
| 3.0.9 | parser-ng-parquet, select over Parquet in parser-ng-sql, a much faster MathExpression constructor, and a complete BaseNFunction. |
| 3.0.9.2 | The strict flag on MathExpression. |
Same MathExpression syntax at every tier. You scale up by choosing a different evaluator, never by rewriting the expression.
Measured on a 2015 Dell Inspiron 5759 with an Intel Core i7-6500U CPU @ 2.50GHz (4 logical CPUs, about 2.6GHz).
| Execution mode | Throughput capacity (evaluations / sec) |
|---|---|
| Standard Mode | 3,000,000 to 10,000,000 |
| Turbo Mode | 10,000,000 to 90,000,000 |
| Bulk Turbo (Single Core) | 200,000,000+ |
| Bulk Turbo (Parallel Workers) | Even higher (hardware saturation) |
| GPU Bulk Turbo (Massive Parallelism) | Highest (hardware saturation) |
- SwiGLU kernel:
1.8 ns / element - GELU kernel:
2.1 ns / element - Custom expressions: highly scalable, consistently executing multiple times faster than standard Janino scalar compilations on single-core setups.
Full measured breakdowns, including GPU throughput at scale, live in BENCHMARK_RESULTS.md.
- Calculus & advanced algebra: symbolic differentiation (
diff), automatic differentiation (autodiff) to arbitrary order, numerical integration (intg), and full matrix algebra: determinants,eigvalues,adjoint,cofactor,echelon,linear_sys, matrix division. - Differential equations:
diffeqn,diffeqnPath,diffeqnHOanddiffeqnPathHOfor single equations, higher-order equations and systems. See DIFF_ENGINE.md. - Vectorized orchestration: hardware vector registers via
jdk.incubator.vector, with a smooth Android-compliant scalar fallback. - Versatile execution architecture: logical and conditional expressions (
if(cond, a, b)), user-defined functions (f(x,y)=...and the anonymous@(x,y)...form), equation root solvers (quadratic,t_rootfor cubics via Tartaglia, generalroot), point, line and function rotation (rot), and real-time geometric plotting buffers. - Global and local function scopes: give each
MathExpression(or group of them) its own private function registry withFunctionScope, shadowing or isolating from the global one, with zero allocation on the hot paths. See global and local function scopes. - Mixed-content arrays: the
ARRAYtype holds numbers and strings side by side. See theARRAYtype section below. - Zero dependencies: portable and self-contained across Java SE, Android, and legacy JavaME profiles.
ParserNG has always had @(dim)(...) vector and matrix literals, but those are strictly numeric, backed by a Matrix. 3.0.8 adds a sibling: the ARRAY type, using the exact same @(dim)(...) syntax, but able to hold a genuine mix of content: numbers and strings side by side, not just numbers.
MathExpression m = new MathExpression("a=@(4)('3.14', 5, \"I am here\", 32.34)");
m.solve();
String[] array = FunctionManager.lookUp("a").getArray();
// array = ["3.14", "5", "I am here", "32.34"]An array literal accepts both single and double-quoted strings ('3.14' and "I am here" are equally valid) alongside bare numeric literals. ParserNG figures out on its own whether the whole @(dim)(...) is a pure numeric VECTOR (still Matrix-backed, unchanged from before) or a mixed ARRAY, and compiles it accordingly. Like every other named function-valued literal, an ARRAY is registered with FunctionManager under its assigned name and retrieved the same way any Function is. getArray() sits right alongside the existing getMatrix() accessor and always returns a String[], since that is the one representation that can hold whatever mix of numbers and strings the literal contained.
This is what makes the differential-equation engine's system-solving support possible. Each equation in an explicit system (see DIFF_ENGINE.md) is a full algebraic expression, not a constant, and not something ParserNG can evaluate eagerly the way a plain number can. Wrapping the set of equations in an ARRAY of quoted strings lets ParserNG treat "here are N equations" as a single, ordinary, eagerly-resolvable literal argument. It is the same trick that already worked for a numeric y0 vector, extended to a type that can hold text.
Until now every MathExpression in the process shared one function registry, FunctionManager.FUNCTIONS. That is still the default, and nothing changes for code that ignores this section. But when many expressions, tenants, plugins or tests must each define their own f(x), a single shared registry means they overwrite one another. A FunctionScope is a private registry that sits in front of the global one.
FunctionManager.add("f(x)=x+1"); // global, exactly as before
FunctionScope mine = FunctionScope.create(); // a new, empty local scope
new MathExpression("f(x)=x*10;f(2)", mine).solve(); // "20.0" defined in `mine`, shadowing the global f
new MathExpression("f(2)", mine).solve(); // "20.0" `mine` remembers its own f
new MathExpression("f(2)").solve(); // "3.0" the global f was never touched| Lookup | The expression's scope first, then its parent, and so on up to the global scope. The first match wins, so a local function shadows a global one of the same name. |
| Definition | f(x)=..., f=@(x)..., matrices (M=@(2,2)(...)), vectors, arrays and anonymous functions all go into the expression's own scope, never into a parent. |
| Deletion | Only removes from the scope you are in. A local scope can never delete a global function, or a global variable. |
| Lexical | A function runs in the scope it was defined in, whoever calls it. A global g(x)=f(x)+1 keeps using the global f, even when called from a scope that defines its own f. A local g that calls f uses the local f. This also holds when the scanner inlines g's body into the caller: matrices and function handles named in the body are pinned to the function the body meant (via hidden per-scope aliases), so they cannot leak to a same-named definition in the caller's scope. |
| No scope given | The global scope, which is the old behaviour. FunctionManager.FUNCTIONS remains the global map. |
// An expression that belongs to a scope
MathExpression me = new MathExpression("f(3)", mine);
me.solve();
me.getFunctionScope(); // `mine`
// A scope you can define things in directly
mine.add("g(x)=x^2"); // local
mine.solve("h(x)=g(x)+1; h(3)"); // "10.0", and h is local too
// Where do names come from?
mine.lookUp("f"); // local f if there is one, else the global f
mine.lookUpLocal("f"); // only what `mine` itself defines
mine.localNames(); // names defined in `mine`
mine.remove("f"); // the global f is visible againA few shapes, depending on how much you want to share:
FunctionScope.create(); // child of the global scope: sees global functions, adds its own
FunctionScope.create(parent); // child of another scope: scopes nest
FunctionScope.isolated(); // no fallback at all: global user functions are invisibleGive each expression its own scope for full separation (100 expressions, 100 different f), or share one scope between many expressions that should agree on their definitions.
Code that predates scopes, or that calls the static FunctionManager API, can be pointed at a scope for a block:
try (FunctionScope.Binding b = mine.activate()) {
FunctionManager.add("k(x)=x+7"); // lands in `mine`
new MathExpression("k(1)").solve(); // an expression created here joins `mine`
} // the previous scope is restoredParserNG resolves function names through the static FunctionManager in a couple of hundred places, so rather than thread a scope argument through all of them, each thread has an active scope and every FunctionManager method reads and writes it. A MathExpression that belongs to a scope makes it active while it parses, compiles and evaluates, then puts the previous one back. An expression created while a scope is active (a function body, a nested sub-expression) joins that scope automatically; that is what makes scoping lexical. Compiled (compileTurbo()) expressions carry their scope with them and keep working on any thread. The scanner's cached vocabulary is kept per scope, so a scope's function names are recognised without ever appearing in the global one.
Scoping is built to cost nothing when unused and next to nothing when used.
- No allocation, ever, on the hot paths. Scope lookup, scope switching, and evaluation of scoped expressions all allocate zero bytes per call (checked by a test that measures per-thread allocation).
- Unscoped code pays one volatile read. Until the first local scope is created in the process, the only added cost is a single flag check per evaluation. Global compiled expressions run at the same ~5 ns as before.
- Local lookups are lock-free. A local scope is a
ConcurrentHashMap, so reading it takes no monitor; a lookup is about 10 ns against about 24 ns for the global (synchronized) map. - Compiled expressions that call no user functions are returned unwrapped, with no scope handling at all.
Measured on one machine (JDK 21, best of seven runs of two million calls); your numbers will differ, the proportions should not:
| Operation | Before | After |
|---|---|---|
solveGeneric, global expression, no scopes in use |
12.3 ns | 12.9 ns |
compiled global expression, applyScalar |
5.4 ns | 5.4 ns |
compiled scoped expression, applyScalar |
n/a | 5.5 ns |
| global function lookup | 23.7 ns | 23.5 ns |
| local function lookup | n/a | 10.2 ns |
Once a local scope exists anywhere in the process, evaluating an expression costs one extra thread-local read (about 3 ns), because a global expression has to be able to tell it is not running inside a local scope.
- Only functions are scoped. Variables keep their existing model: a per-expression registry backed by the global
VariableManager. - As in the global registry, a function keeps what its body referred to when it was defined. If
pp(x)=f(x)+5is defined andfis redefined afterwards,ppstill uses the oldf. This is unchanged from before; it applies inside a scope exactly as it does globally. - Serialization does not carry the scope: a deserialized
MathExpressionis global. - A
MathExpressionis still not thread-safe; give each thread its own, as before. Scopes themselves are safe to share. - Code that reads
FunctionManager.FUNCTIONSdirectly sees the global functions only.
Same expression syntax, five different execution tiers. Pick based on how hot the loop is.
No compile step. Right for one-off evaluations, or expressions that change shape every call.
MathExpression me = new MathExpression("f(x,y,z)=3*x+4*y+sin(z-2);f(3,4,5)");
System.out.println(me.solve());
// Conditionals and multiple return types come free:
MathExpression cond = new MathExpression("if(3*x+7>5, sin(x), -3)");
MathExpression.EvalResult r = cond.solveGeneric(-42.0); // r.scalar, r.vector, r.matrix, r.boolVal, r.textResTurboEvaluatorFactory picks the right one for you; reach for a specific evaluator directly when you already know your shape (for example, point/line/function rotation compiles cleanly on ScalarTurboEvaluator1).
MathExpression me = new MathExpression("rot(@(1,3)(0,2,0), pi, @(1,3)(0,0,0), @(1,3)(1,0,0))");
FastCompositeExpression turbo = new ScalarTurboEvaluator1(me).compile();
double result = turbo.applyScalar(new double[0]);
// Or let the factory decide:
FastCompositeExpression auto = TurboEvaluatorFactory.getCompiler(me).compile();Matrices are first-class function values. Solve, invert, decompose, all compiled.
Matrix m = new Matrix(coefficientData, n, n + 1);
m.setName("M");
new Function(m); // registers M for lookup by name
MathExpression expr = new MathExpression("linear_sys(M)");
FastCompositeExpression turbo = TurboEvaluatorFactory.getCompiler(expr).compile();
Matrix solution = turbo.applyMatrix(new double[0]);MathExpression me = new MathExpression(
"(1 / (x1 * sqrt(2 * 3.14159))) * exp((-(x2 - x3)^2) / (2 * x1^2))");
var evaluator = (SIMDVectorTurboEvaluator.SIMDVectorCompositeExpression)
new SIMDVectorTurboEvaluator(me).compile();
double[][] inputs = new double[3][totalElements]; // x1, x2, x3
double[] out = new double[totalElements];
evaluator.applyBulk(inputs, out); // tail elements auto-masked if totalElements isn't lane-alignedMathExpression me = new MathExpression("2*x^2-3*x+1");
VectorTurboEvaluator vte = new VectorTurboEvaluator(me);
try (GpuCompositeExpression gpu = GpuExpressionBridge.from(vte, GpuBackend.OPENCL)) {
float[] in = /* ... */;
float[] out = new float[in.length];
gpu.applyBulk(in, out); // native float32 kernel, no double bridging
}The full GPU quick-start, including multi-vendor device selection, is at the top of this document.
Module choice maps directly onto the evolution table above: pick the highest tier you need, and each module pulls in everything below it. All ParserNG modules share one version number, and the examples below use 3.1.0.
Core interpreter, Turbo scalar and matrix tiers, and BulkTurboEvaluator for mechanically-sympathetic bulk loops with an inbuilt worker system:
<dependency>
<groupId>com.github.gbenroscience</groupId>
<artifactId>parser-ng</artifactId>
<version>3.1.0</version>
</dependency>BulkTurboEvaluator.BatchedVectorCompositeExpression evaluator =
(BulkTurboEvaluator.BatchedVectorCompositeExpression) new BulkTurboEvaluator(me).compile();
double[] out = new double[1];
evaluator.applyBulk(new double[]{5, 4, 1}, out);Adds VectorTurboEvaluator and SIMDVectorTurboEvaluator:
<dependency>
<groupId>com.github.gbenroscience</groupId>
<artifactId>parser-ng</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.github.gbenroscience</groupId>
<artifactId>parser-ng-simd</artifactId>
<version>3.1.0</version>
</dependency>Adds the SIMDCommand series and the GPU bulk evaluators (CUDA, OpenCL and Metal):
<dependency>
<groupId>com.github.gbenroscience</groupId>
<artifactId>parser-ng</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.github.gbenroscience</groupId>
<artifactId>parser-ng-simd</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.github.gbenroscience</groupId>
<artifactId>parser-ng-gpu-simd</artifactId>
<version>3.1.0</version>
</dependency>| Module | What it does |
|---|---|
parser-ng-arrow |
Zero-copy evaluation, filtering and projection over Apache Arrow memory, with SIMD and GPU modes. |
parser-ng-sql |
SQL select over Arrow data and, since 3.0.9, over Parquet files (local and remote). |
parser-ng-parquet |
Fast reading of Parquet files, local and remote (for example S3). |
All ParserNG modules share one version number, so a new module always starts at the parent's latest release. Add the ones you need next to parser-ng:
<dependency>
<groupId>com.github.gbenroscience</groupId>
<artifactId>parser-ng-arrow</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.github.gbenroscience</groupId>
<artifactId>parser-ng-sql</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.github.gbenroscience</groupId>
<artifactId>parser-ng-parquet</artifactId>
<version>3.1.0</version>
</dependency>Each module's own README has the details.
<dependency>
<groupId>com.github.gbenroscience</groupId>
<artifactId>parser-ng-bench</artifactId>
<version>3.1.0</version>
</dependency>| Category | Supported mathematical tokens & functions | Turbo optimization support |
|---|---|---|
| Arithmetic operators | +, -, *, /, ^, %, and, or, ==, != |
Full hardware mapping |
| Trigonometric functions | sin, cos, tan, asin, acos, atan, sinh, cosh |
Full hardware mapping |
| Calculus engines | diff (symbolic engine), intg (numerical boundaries) |
Yes |
| Differential equations | diffeqn, diffeqnPath, diffeqnHO, diffeqnPathHO. See DIFF_ENGINE.md. |
Yes |
| Matrix algebra | det, eigvalues, eigvec, adjoint, linear_sys |
Optimized linear path |
| Statistical functions | avg, variance, rms, sort, erf, erfc |
Yes |
| Activation functions | gelu, geglu, swiglu |
Yes |
ParserNG is used by 167 organizations to power mission-critical math visualization, analytical pipelines, and platform tooling. Special thanks to Jiri Vanek for contributing ExpandingParser and integrating it into the jenkins-report-generic-chart-column plugin ecosystem.
Production infrastructures that need predictable performance, safety assurances, and expert engineering access can upgrade to ParserNG Enterprise:
- Priority operational support: 24 to 48-hour SLAs for immediate bug tracking, profiling, and compliance vulnerabilities.
- GraalVM Native Image deployment: turn-key integration configurations for compiling down to fast native binaries.
- Direct consultative access: architectural reviews, customized functions (extensions), and hand-tailored vector and GPU kernel design directly from the author.
📧 Contact for corporate licensing and consultations: gbenroscience@gmail.com
☕ Support open source development: GitHub Sponsors corporate tiers. The SIMD engine described above was Enterprise-only until the 3.0.8 release. Sponsorship is what funds moving the next tier out from behind that wall.
- Deep-dive benchmarking logs: BENCHMARK_RESULTS.md, comprehensive execution breakdowns versus competitor runtimes.
- High-fidelity graphical plotting: GRAPHING.md, render configuration rules for JavaFX, Swing, and Android surfaces.
- Bulk vectorization blueprints: BULK.md, optimization techniques for massive array processing.
- Differential equations: DIFF_ENGINE.md, full
diffeqn/diffeqnPath/diffeqnHO/diffeqnPathHOsyntax, solver selection guide, and result-capture patterns. - Release notes: LATEST.md, change logs and technical notes for the latest release.
- MORE.md: even more to know.
- Hello world and original readme: the original readme for pre-1.0 versions, with a lot of examples that are still valid.
Developed with mathematical rigor and mechanical sympathy by Gbemiro Jiboye (@gbenroscience).