A Zig port of libmpdec providing arbitrary-precision decimal arithmetic with IEEE 754 semantics, similar to Python's decimal module.
- Package name:
zig_libmpdec - Minimum Zig version:
0.16.0
While the authors believe this project is mostly functional and may be usable to you, it was primarily designed while testing LLM/LRM security and memory safety checking on zig 0.16 while it was too new to be supported in various local and cloud models.
The code was extracted and modified from a larger private project and modified expressly to fit the above needs.
The code was mostly hand generated but many fixes were guided by LLM suggestions.
The coding style and comment style specifically target the Qwen 3.6 models preferences and limitations, to both guard against unnecessary changes and to ensure that a zed-agent had the highest chances of success. At the time of development Qwen 3.6 was the only local model that we found that wasn’t tripped up with the zig 0.16 changes like std.Io.
This project is intentionally licensed as CC0-1.0 so that if someone wished they could fork and optimize it. But please do not expect optimizations to be pulled into this repo.
The fuzzing code is LLM generated slop and didn't work with zig 0.16 it is being left in to test how quickly models adapt once zig 0.16.1 is released.
The files that are known to be unusable slop are:
/src/fuzz.zig/src/fuzz_targets.zig
They were included here so some friends could mess around with them, just dont run build test --fuzz and they won't get in the way.
zig fetch --save git+https://github.com/gdahlm/zig-libmpdec.gitThis adds the dependency to your build.zig.zon. Then wire it up in your build.zig:
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Get the dependency from your zon file
const libmpdec_dep = b.dependency("zig_libmpdec", .{
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zig_libmpdec", .module = libmpdec_dep.module("zig_libmpdec") },
},
}),
});
b.installArtifact(exe);
const run_step = b.step("run", "Run the app");
run_step.dependOn(&b.addRunArtifact(exe).step);
}If you're developing both projects locally, reference it by path in your build.zig.zon:
// In build.zig.zon dependencies table
.dependencies = .{
.zig_libmpdec = .{
.path = "../zig-libmpdec",
},
},Then use the same b.dependency(...) pattern in your build.zig as shown above.
Clone the repository and run:
git clone https://github.com/gdahlm/zig-libmpdec.git
cd zig-libmpdec
# Build the example executable
zig build
# Run tests
zig build test
# Run the example app
zig build runconst std = @import("std");
const libmpdec = @import("zig_libmpdec");
/// Helper: format a decimal and return an owned string.
fn fmt(allocator: std.mem.Allocator, d: *const libmpdec.Decimal) ![]u8 {
return d.format(allocator);
}
pub fn main(init: std.process.Init) !void {
const arena = init.arena;
defer arena.deinit();
const allocator = arena.allocator();
// Create a decimal context with 28 digits of precision
var ctx = libmpdec.Context.init(28);
// Parse decimal strings
var a = try libmpdec.Decimal.parse(allocator, "1.23456789");
defer a.deinit();
var b = try libmpdec.Decimal.parse(allocator, "9.87654321");
defer b.deinit();
// Perform arithmetic
var sum = try libmpdec.decAdd(&ctx, &a, &b);
defer sum.deinit();
// Format the result
std.debug.print("{s}\n", .{try fmt(allocator, &sum)});
// Output: 11.11111110
}| Type | Description |
|---|---|
Decimal |
Arbitrary-precision decimal number (sign, coefficient, exponent) |
MPInt |
Arbitrary-precision integer used for coefficients |
Context |
Holds precision, rounding mode, and exponent limits |
RoundingMode |
Enum: round_half_even, round_half_up, round_towards_zero, etc. |
DecimalSpecial |
Enum: normal, zero, nan, sNaN, inf |
All arithmetic functions take a *Context and return a new Decimal:
| Function | Description |
|---|---|
decAdd(ctx, a, b) |
Addition |
decSub(ctx, a, b) |
Subtraction |
decMul(ctx, a, b) |
Multiplication |
decDiv(ctx, a, b) |
Division |
decNeg(ctx, a) |
Negation |
decAbs(ctx, a) |
Absolute value |
decCopy(ctx, a) |
Copy a decimal |
decCmp(a, b) |
Comparison (returns std.math.Order) |
| Method | Description |
|---|---|
Decimal.parse(alloc, str) |
Parse from string (e.g. "1.23e-4", "NaN", "Infinity") |
Decimal.fromInt(n) |
Create from integer |
Decimal.fromUInt(n) |
Create from unsigned integer |
Decimal.nan() / Decimal.inf() |
Create special values |
dec.isZero() / dec.isInfinite() / dec.isNaN() |
Predicate checks |
dec.deinit() |
Free allocated resources |
dec.format(buf, options) |
Format to string buffer |
Decimal.parse allocates memory for the coefficient. Always call deinit() on Decimal instances when you're done with them. The returned Decimal from arithmetic operations owns its own allocation and must also be deinitialized.
Using an ArenaAllocator is recommended when performing many operations, as it simplifies cleanup:
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
var a = try libmpdec.Decimal.parse(arena.allocator(), "10");
// No need to individually deinit - arena cleans up everythingThe Context supports eight IEEE 754 rounding modes:
round_half_even(default, "banker's rounding")round_half_upround_half_downround_towards_zeroround_away_from_zeroround_towards_plus_infround_towards_minus_infround_to_odd
Set it when creating the context:
var ctx = libmpdec.Context.init(28);
ctx.rounding = libmpdec.RoundingMode.round_half_up;zig build testCC0-1.0