Skip to content

Repository files navigation

tinyexpr.zig

ci

Port: codeplea/tinyexpr (C) → Zig 0.16.0. Pin: 4a7456e2eab88b4c76053c1c4157639ccb930e2b. Verify: docker compose up --build verify Demo video: docs/demo/demo.mp4

Default suite 4930/4930 against the Zig library

Status

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.shdocs/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

Equivalence scope

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.

Intentional divergences

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

Architecture (short)

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.

Honest limitations

  • 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.

Benchmarks (headline)

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.

Consumers

  • C: examples/c-consumer/ (unmodified tinyexpr.h + Zig .a / .so)
  • Zig: examples/zig-consumer/ (zig fetch + b.dependency("tinyexpr"))

Native Zig API

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.

Findings / upstream

See docs/findings.md. Filed or confirmed: issue 136, issue 105, issue 141, issue 142, PR 143, PR 144.

Evidence and docs

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

Build

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 run

Licence

zlib. Altered derivative of TinyExpr. See LICENSE and THIRD_PARTY.md.

Docs

Quick path: docs/quickstart.md. FAQ: docs/faq.md.

About

Zig port of codeplea/tinyexpr with a preserved C ABI and the original smoke suite linked against the Zig library

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages