Port: codeplea/tinyexpr (C) → Zig 0.16.0.
Pin: 4a7456e2eab88b4c76053c1c4157639ccb930e2b.
Verify: docker compose up --build verify
Demo video: docs/demo/demo.mp4
Original smoke.c / minctest.h (byte-identical) link against the Zig static
library through a preserved C ABI and pass in every flag combination. Numbers
below come from docs/evidence/scorecard.json (regenerated by
tools/scorecard.py); scripts/check-readme-claims.sh fails CI if this file
disagrees with the scorecard.
| Config | Zig-linked smoke | C-linked smoke |
|---|---|---|
| default | 4930/4930 | 4930/4930 |
pow (TE_POW_FROM_RIGHT) |
4924/4924 | 4924/4924 |
natlog (TE_NAT_LOG) |
4934/4934 | 4934/4934 |
pow_natlog (smoke_pr) |
4928/4928 | 4928/4928 |
Escape hatches: 16 total in src/c_abi.zig (799 audit-surface lines, 6 exported te_* functions → ~2.67 hatches per export), gated by tools/count-escape-hatches.py (threshold 20).
Cross-compile (scripts/cross-compile.sh → docs/evidence/cross-compile.json):
each target is labelled builds_only, unit_tests_pass, or
full_suite_passes (not collapsed to a checkmark).
| Target | Status |
|---|---|
| x86_64-linux-gnu | builds_only |
| x86_64-linux-musl | builds_only |
| aarch64-linux-gnu | full_suite_passes |
| x86_64-windows-gnu | builds_only |
| aarch64-macos | builds_only |
| riscv64-linux | unit_tests_pass |
| wasm32-wasi | unit_tests_pass |
Bit-exact C↔Zig ABI agreement is claimed for the C locale (LC_NUMERIC), at
nesting depths below the port bound, on inputs where the C original does not
invoke undefined behaviour. Detail: docs/findings.md.
| Input class | C original | Port | Why |
|---|---|---|---|
| Deep nested parentheses | Stack overflow (issue 136) | Bounded parse error / NULL compile | Fail closed (D008) |
-Dfix-fac-overflow=true |
Truncates non-integer fac args |
Returns NaN for non-integers | Optional hardening; default matches smoke (D006) |
Decimals after comma-decimal setlocale |
strtod follows LC_NUMERIC |
Same via libm.strtod |
Bit-exact parity (D025); scope to C locale |
High-bit first byte on signed char |
UB at isalpha/isdigit (issue 105) |
No C ctype on plain char |
Avoid copying that UB |
One recursive-descent grammar is parameterized over a comptime builder. The
same parse code emits either the native tagged-union AST or the C-layout
te_expr tree. Escape hatches (@ptrCast, allocator bridging) live only in
src/c_abi.zig. See docs/architecture.md.
- Nesting is bounded (default
-Dmax-nesting=2048); deep nests that crash C return an error instead. - Decimal literals follow
strtod/LC_NUMERIC(shared with C). - Benchmarks were measured in a Linux Docker container on Apple Silicon.
CPU governor and turbo were not controllable. Tail latencies are less
trustworthy than on bare metal. Protocol:
bench/methodology.md. - Allocator-fault injection found no C crash under the harness we ran
(honest negative). Details in
docs/findings.md.
Fair comparison: C -O2 vs Zig ReleaseSafe (safety kept). Both Safe and
Fast are always reported. Host: Linux aarch64 container. Full distributions
and plots: bench/results.json, bench/plots/, bench/methodology.md.
| Metric | C -O2 | Zig ReleaseSafe | Zig ReleaseFast |
|---|---|---|---|
te_interp arithmetic p99 (ns) |
8125 | 8209 | 2875 |
te_compile functions p99 (ns) |
19750 | 10750 | 9458 |
te_eval variables p99 (ns) |
42 | 42 | 42 |
| throughput shared4 (expr/s) | 949442.683 | 1209049.9 | 1148760.127 |
RSS ru_maxrss KiB (retained function trees) |
1464 | 1764 | 1468 |
| startup warm p50 (ns) | 1335500 | 1913000 | 976708 |
ReleaseSafe te_interp arithmetic p99 was about 1.03% slower than C on
this host (8209 vs 8125 ns). Startup and RSS are also higher for Safe. We
still headline Safe because those checks are part of the port. Compile p99
and throughput favored Safe here; that is a container measurement, not a
universal claim. Artifact sizes: Safe static archive is multi-megabyte vs
20968 bytes for the C -O2 object; Fast is smaller but still larger than C.
- C:
examples/c-consumer/(unmodifiedtinyexpr.h+ Zig.a/.so) - Zig:
examples/zig-consumer/(zig fetch+b.dependency("tinyexpr"))
const tinyexpr = @import("tinyexpr");
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var err: c_int = 0;
const v = tinyexpr.interp(allocator, "sin(pi/4)", &err);Ownership, errors, variables, functions, closures, and purity: doc comments on
src/root.zig / src/ast.zig, plus docs/architecture.md.
See docs/findings.md. Filed or confirmed:
issue 136,
issue 105,
issue 141,
issue 142,
PR 143,
PR 144.
| Item | Evidence |
|---|---|
| Differential fuzz | docs/evidence/fragments/fuzz-certified.json |
| Escape-hatch gate | CI + count in scorecard |
| Upstream findings | docs/findings.md + upstream URLs |
| Decision log | DECISIONS.md |
| Quickstart / FAQ | docs/quickstart.md, docs/faq.md |
Requires Zig 0.16.0. Prefer Docker. Host builds need cc, Python 3, and
(for enumeration) GNU objcopy.
zig build
zig build test
zig build bench
bash examples/c-consumer/build.sh
cd examples/zig-consumer && zig build runzlib. Altered derivative of TinyExpr. See LICENSE and THIRD_PARTY.md.
Quick path: docs/quickstart.md. FAQ: docs/faq.md.
