Introducing CJC-Lang: Why I’m Building a Deterministic Scientific Computing Language
What CJC-Lang is, why it exists, what it can already do, and where it stands today.
I didn’t set out to build a programming language. I set out to solve a problem.
I spent years working as a lab engineer running experiments on audio devices — collecting data, cleaning it, troubleshooting when results didn’t look right. Was the anomaly real, or was it an environment issue? A script issue? A sensor calibration drift? I learned firsthand how messy data — or messy handling of data — introduces problems that cascade through every stage of analysis. A mistake in the initial data cleaning phase doesn’t just cost time. It costs weeks of downstream work, delays shipping, and erodes confidence in the results.
That experience stuck with me as I moved into data science. In most scientific computing environments, running the same program twice can produce slightly different results. Floating-point reduction order changes between runs. Hash maps iterate in random order. Thread scheduling affects accumulation. RNG state leaks across boundaries. These aren’t bugs — they’re properties of systems that weren’t designed with determinism as a constraint.
I wanted a system where the same program with the same seed produces bit-identical output every time, on every platform. Not approximately the same — exactly the same.
That constraint turned out to touch everything: memory layout, collection types, floating-point arithmetic, random number generation, parallelism. The deeper I went, the clearer it became that bolting determinism onto an existing language would mean fighting the language at every turn. So I built one where determinism is structural.
What Is CJC-Lang?
CJC-Lang — Computation Jacobian Core — is a programming language I’m building from scratch for deterministic scientific computing. It’s written in Rust, organized as a 20-crate workspace, with nearly all functionality implemented from scratch using only the Rust standard library.
The language covers the full compilation and execution pipeline: a lexer, recursive descent parser, type checker, two intermediate representations (HIR and MIR), an optimizer, two execution engines, and a runtime with ~300 built-in functions for math, statistics, linear algebra, machine learning, signal processing, and data manipulation.
Core Ideas
Determinism as Architecture
CJC-Lang enforces determinism at every layer:
- Ordered collections everywhere.
BTreeMapandBTreeSetinstead of hash-based containers. Iteration order is always defined. - Stable floating-point reductions. All summations use Kahan or binned accumulators — order-invariant superaccumulators that produce identical results regardless of evaluation order.
- No FMA in SIMD kernels. Fused multiply-add produces different rounding than separate multiply and add. CJC-Lang avoids FMA to keep results identical across CPU architectures.
- Seeded, explicit RNG. The random number generator is SplitMix64 with explicit seed threading. No global state, no entropy sources.
- Deterministic parallelism. Parallel operations are structured so that results don’t depend on thread count or scheduling.
This isn’t a mode you opt into. It’s how the language works. The determinism guarantee is verified in the test suite — for example, the SciML tests train a physics-informed model twice with the same seed and assert that every parameter is bit-identical:
// From tests/sciml_determinism.rs
let r1 = piml_heat_1d_train(6, 20, 30, 0.01, 1000, 1e-3, 1.0, 10.0, 42);
let r2 = piml_heat_1d_train(6, 20, 30, 0.01, 1000, 1e-3, 1.0, 10.0, 42);
for (i, (&a, &b)) in r1.final_params.iter()
.zip(r2.final_params.iter()).enumerate()
{
assert_eq!(a.to_bits(), b.to_bits(),
"Param {} not bit-identical", i);
}Not approximately equal. Bit-identical. to_bits() compares the raw IEEE 754 representation.
Nearly Zero External Dependencies
The entire system compiles to a 6.4 MB single binary. That one file contains everything: lexer, parser, type checker, two execution engines, optimizer, ~300 builtins, tensor library, linear algebra kernels, regex engine, AD engine, data manipulation DSL, serialization, visualization, and 15+ CLI subcommands.1
This is a deliberate choice. External dependencies introduce version churn, platform variance, and complexity that’s hard to audit. For a system that promises bit-identical results, every component needs to be under direct control.
Dual Execution Engines
CJC-Lang has two execution paths:
- AST interpreter (
cjc-eval): walks the syntax tree directly. Simple, easy to debug, good for prototyping. - MIR executor (
cjc-mir-exec): lowers through HIR → MIR, applies optimization passes (constant folding, dead code elimination), and runs on a register machine.
Both engines must produce identical results for every program. Parity tests enforce this — if the engines disagree, that’s a bug, and the test suite catches it.
NoGC Verification
CJC-Lang includes a static verifier that can prove code paths are free of GC allocations. The NoGC verifier performs transitive call-graph analysis to ensure that annotated functions — and everything they call — operate within reference-counted, copy-on-write memory without triggering GC. This matters for latency-sensitive workloads where GC pauses are unacceptable.
What CJC-Lang Can Do Today
Every example below comes from real files in the repository — demo programs, test fixtures, or integration tests.
Closures and Higher-Order Functions
CJC-Lang supports lambda expressions with capture analysis. The compiler performs lambda lifting during HIR lowering, converting closures into top-level functions with explicit environment parameters:
fn apply(f: fn(i64) -> i64, x: i64) -> i64 {
f(x)
}
fn main() -> i64 {
let double = |x: i64| x * 2;
let result: i64 = apply(double, 21);
print(f"result: {result}");
result
}
The capture analysis feeds into the NoGC verifier — if a closure captures a reference that would require GC, the verifier flags it.
Structs, Enums, and Pattern Matching
CJC-Lang supports algebraic data types with structural destructuring:
struct Point {
x: f64,
y: f64
}
fn distance(p: Any) -> f64 {
sqrt(p.x * p.x + p.y * p.y)
}
let p = Point { x: 3.0, y: 4.0 };
print(distance(p));
Match expressions support literal patterns, wildcards, and structural destructuring for tuples and structs. There’s also an Option type with Some/None and unwrap/unwrap_or.
Tensors and Linear Algebra
This is where CJC-Lang starts to show its numerical computing focus. Tensors are N-dimensional arrays with zero-copy COW (copy-on-write) buffers:
let A = Tensor.from_vec(
[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
[2, 3]
);
let B = Tensor.from_vec(
[7.0, 8.0,
9.0, 10.0,
11.0, 12.0],
[3, 2]
);
let C = matmul(A, B);
print(C.shape()); // [2, 2]
assert_eq(C.get([0, 0]), 58.0);
The runtime provides the full suite: SVD, QR, Cholesky, eigenvalue decomposition, matrix exponential, Schur decomposition, condition number, and multiple matrix norms. All floating-point reductions go through Kahan or binned summation.
Neural Network Forward Pass
CJC-Lang can express multi-layer neural networks with struct-based layer definitions and a pipe operator (|>) for readable data flow:
struct Layer {
weights: Tensor,
bias: Tensor
}
fn forward(layer: Layer, input: Tensor) -> Tensor {
matmul(input, layer.weights) |> add_bias(layer.bias)
}
The runtime provides built-in ML operations: batch_norm, dropout_mask, sigmoid, relu, leaky_relu, attention, softmax, learning rate schedulers, loss functions, and regularization. I’ll go deeper into the neural network architectures I’ve built and verified in CJC-Lang in the next post.
Gradient Descent and Automatic Differentiation
A simple but complete optimization loop — gradient descent on f(x) = (x - 3)^2:
fn f(x: f64) -> f64 {
(x - 3.0) * (x - 3.0)
}
fn grad_f(x: f64) -> f64 {
2.0 * (x - 3.0)
}
let x = 0.0;
let lr = 0.1;
let i = 0;
while i < 100 {
let g = grad_f(x);
x = x - lr * g;
i = i + 1;
}
print("Final x:", x); // ~3.0
assert(abs(x - 3.0) < 0.001);
For more complex cases, CJC-Lang has a full automatic differentiation engine — forward-mode via dual numbers and reverse-mode via tape-based gradient computation.
Statistics and Distributions
The runtime includes 35+ statistical functions, 10 probability distributions, and 12 hypothesis tests:
let data = [23.1, 25.4, 22.8, 24.5, 26.1, 23.9, 24.2, 25.0, 22.5, 24.8];
let data_t = Tensor.from_vec(data, [10]);
print(data_t.mean());
print(median(data));
print(sd(data));
// Normal distribution
print(normal_cdf(1.96)); // P(Z < 1.96)
print(normal_ppf(0.975)); // quantile function
// Hypothesis testing
let test_result = t_test(data, 24.0);
print(test_result);
Beyond the basics: robust statistics (trimmed_mean, winsorize, mad), rank correlations (spearman_cor, kendall_cor, partial_cor), weighted statistics, and non-parametric tests (wilcoxon_signed_rank, kruskal_wallis, mann_whitney).
Distributions include normal, beta, gamma, exponential, F, Weibull, Poisson, binomial, chi-squared, and Student’s t — each with PDF/PMF, CDF, and where applicable, quantile functions.
Data Manipulation
CJC-Lang includes a tidyverse-inspired data DSL with 73+ operations:
let csv_data = "name,age,city\nAlice,30,NYC\nBob,25,LA\nCharlie,35,NYC\n";
let df = Csv.parse(csv_data);
let view = df.view();
// Filter and select
let pred = dexpr_binop(">", col("age"), 28);
let filtered = view.filter(pred);
let selected = filtered.select(["name", "city"]);
The data layer supports typed columns, joins (inner, left, right, anti, semi), pivoting, window functions, and lazy evaluation with COW semantics.
Beyond Neural Networks
CJC-Lang isn’t just a machine learning language. The numerical infrastructure supports a broader range of scientific computing — and this is where the determinism guarantee pays the biggest dividends. When you’re debugging a failed ODE integration or a clustering result that doesn’t match your colleague’s run, knowing the computation is reproducible eliminates an entire class of “was it the code or was it the environment?” questions.
| Domain | Key Capabilities |
|---|---|
| ODE/PDE Solvers | Euler, RK4, adaptive RK45, 1D Laplacian, diffusion stepping, adjoint sensitivity |
| Optimization | Gradient descent, BFGS, L-BFGS, Nelder-Mead, bisection, Brent, Newton, secant |
| Regression | OLS, weighted least squares, logistic regression, polynomial fitting |
| Clustering | K-means (k-means++ init), DBSCAN, agglomerative (4 linkage methods), PCA |
| Time Series | ACF, PACF, EWMA, seasonal decomposition, stationarity tests (ADF, KPSS, Phillips-Perron) |
| Interpolation | Cubic natural splines, linear, nearest-neighbor, Horner polynomial evaluation |
let result = ode_solve_rk45(
|t: f64, y: f64| -0.5 * y, // dy/dt = -0.5y (exponential decay)
0.0, // t_start
10.0, // t_end
1.0, // y_initial
1e-6 // tolerance
);
print("Final y:", result); // ~0.00674 (matches exp(-5))
What’s Native — and Why That Matters
Everything mentioned above runs inside a single binary with nearly zero external dependencies. Every component is under direct control, which means determinism guarantees extend through the entire stack — from CSV parsing through model training through serialization.
| Component | What It Does | Why Native Matters |
|---|---|---|
Regex (cjc-regex) |
Thompson NFA engine — O(n) linear time, no backtracking | Eliminates ReDoS vulnerabilities and supply chain risk |
| RNG (SplitMix64) | Seeded, explicit state, fork() for parallel streams |
Same seed = same output on every platform |
Visualization (cjc-vizor) |
Grammar-of-graphics with SVG/BMP output | No subprocess, no JS engine, no network calls |
DataFrame (cjc-data) |
73+ tidyverse-style ops, lazy eval, typed columns | Grouping and aggregation inherit determinism automatically |
Serialization (cjc-snap) |
Canonical binary format with SHA-256 content hashing | Checkpoints are exact; snap_hash() enables verification across runs |
Accumulators (cjc-repro) |
Kahan compensated and binned superaccumulators | Order-invariant reductions — the mechanism behind bit-identical floating-point results |
No pip install. No npm install. No version conflicts. Just the binary, the source file, and the seed.
The CLI Toolchain
CJC-Lang ships with 15+ CLI subcommands:
cjcl run program.cjcl # execute a program
cjcl run program.cjcl --seed 42 # explicit RNG seed
cjcl run program.cjcl --mir-opt # MIR-optimized execution
cjcl check program.cjcl # type-check without running
cjcl repl # interactive REPL
cjcl trace program.cjcl # execution tracing
cjcl bench program.cjcl # benchmarking
cjcl doctor # health/validation checksAdditional commands include inspect, schema, flow (control flow graphs), drift (determinism drift detection), mem (memory profiling), pack (binary serialization), and forge (code generation).
Why CJC-Lang Is Still in Beta
The core is solid: the compiler pipeline works end-to-end, both execution engines agree, the test suite is extensive, and the runtime covers a wide surface area. But several areas need more work:
- Type inference is limited — most types require explicit annotation
- Generics are parsed but not fully enforced at the type level
- The module system exists but needs refinement for multi-file projects
- REPL lacks readline support
- Optimizer has room for more passes beyond constant folding and DCE
- Documentation is sparse outside of test coverage
I’m not rushing to call this production-ready. The point is to build something correct first, then make it convenient.
What Comes Next
The near-term roadmap includes:
- If-expressions: converting
iffrom a statement to an expression (let x = if cond { a } else { b }) - Default parameters and variadic functions: making function signatures more flexible
- MIR-integrated autodiff: ensuring gradient computation works through the optimized execution path
- Module system improvements: reliable multi-file program support
- Sparse linear algebra: eigenvalue solvers (Lanczos, Arnoldi) with deterministic iteration
Footnotes
The only external dependencies are in the optional language server (
cjc-analyzer) and therayoncrate for parallel tensor operations. The core 18 crates have zero external dependencies.↩︎