Skip to content

Deprecated allocator initialization + tests fix LLVM crash by AI #10

Merged
koko1123 merged 4 commits intoStrobeLabs:mainfrom
Mario-SO:main
Feb 27, 2026
Merged

Deprecated allocator initialization + tests fix LLVM crash by AI #10
koko1123 merged 4 commits intoStrobeLabs:mainfrom
Mario-SO:main

Conversation

@Mario-SO
Copy link
Contributor

@Mario-SO Mario-SO commented Feb 27, 2026

What

This contribution ended up as two commits:

  1. Migrate GPA initialization in docs/examples from GeneralPurposeAllocator(.{}){} to typed .init.
  2. Fix a Zig 0.15.2 LLVM crash in unit conversion (float <-> u256) that was discovered while running integration tests.

⚠️ The fix for the second commit was implemented by ai, and afterwards, test commands pass with no errors

Why

  • The GPA change aligns examples/docs with current Zig initialization style and the direction from Replace deprecated default initializations with decl literals ziglang/zig#21287.
  • While validating tests, integration builds crashed with LLVM ERROR: Unsupported library call operation!. The crash was triggered by direct @intFromFloat/@floatFromInt with u256 in units.zig, so tests could not run reliably on Zig 0.15.2.

⚠️ AI made some minimal reproductions of this, you can also run them with this commands

# Reproduces the LLVM crash
cat > /tmp/zig_u256_float_crash2.zig <<'EOF'
const std = @import("std");

const ETHER: u256 = 1_000_000_000_000_000_000;

fn parseEther(ether: f64) u256 {
    const wei_f = ether * @as(f64, @floatFromInt(ETHER));
    return @intFromFloat(wei_f);
}

test "parse ether" {
    const x = parseEther(1.0);
    try std.testing.expect(x > 0);
}
EOF

zig test /tmp/zig_u256_float_crash2.zig
# Control case: simple float<->u256 cast that does NOT crash (on this machine)
cat > /tmp/zig_u256_float_ok.zig <<'EOF'
const std = @import("std");

test "simple float u256 conversions" {
    const a: u256 = @intFromFloat(@as(f64, 1.0));
    const b: f64 = @floatFromInt(a);
    try std.testing.expect(b == 1.0);
}
EOF

zig test /tmp/zig_u256_float_ok.zig

How

  • Updated GPA initialization in (Human made):
    • examples/02_check_balance.zig
    • examples/04_send_transaction.zig
    • examples/05_read_erc20.zig
    • docs/content/docs/introduction.mdx
  • Reworked conversions in src/utils/units.zig (Ai Made):
    • Replaced direct float<->u256 casts with helper conversions via hi/lo u128 halves.
    • Updated parseEther, parseGwei, formatEther, formatGwei to use the safe path.
  • Validation:
    • zig build test passes
    • zig build integration-test passes
    • zig test --dep eth -Mroot=tests/integration_tests.zig -Meth=src/root.zig passes

Checklist

  • zig build test passes
  • zig fmt --check src/ tests/ passes
  • New functionality includes tests
  • No external dependencies added
  • CHANGELOG.md updated (if user-facing change)

EDIT: Recommendation from Kristoff is to wait to 0.16 because it may have being solved already, anyway, i think that tests on current codebase should pass at least.

Summary by CodeRabbit

  • Documentation

    • Updated examples and docs to reflect a clearer, consistent initialization pattern.
  • Refactor

    • Centralized and improved numeric conversion logic for more accurate ether/gwei parsing and formatting, including platform robustness fixes.
  • Tests

    • Added tests for very large values and edge cases; adjusted expectations to account for floating-point precision limits.

@vercel
Copy link

vercel bot commented Feb 27, 2026

@Mario-SO is attempting to deploy a commit to the koko-strobeorg's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57ea087 and f750137.

📒 Files selected for processing (1)
  • src/utils/units.zig
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/units.zig

📝 Walkthrough

Walkthrough

Allocator initialization syntax updated in docs/examples; src/utils/units.zig gains f64↔u256 conversion helpers, refactors parse/format functions to use them, and adds tests. Public APIs unchanged.

Changes

Cohort / File(s) Summary
Allocator Initialization Updates
docs/content/docs/introduction.mdx, examples/02_check_balance.zig, examples/04_send_transaction.zig, examples/05_read_erc20.zig
Replace var gpa = std.heap.GeneralPurposeAllocator(.{}){} with explicit typed initializer var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;. Usage and deinit calls unchanged.
Floating-Point Conversion Helpers & Tests
src/utils/units.zig
Add internal f64 constants (ETHER_F64, GWEI_F64, TWO_POW_128_F64) and helpers (u256ToF64, f64ToU256) that convert via 128-bit decomposition to avoid aarch64 LLVM issues; refactor parseEther, parseGwei, formatEther, formatGwei to use these helpers and add large-value tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I nibble at bytes beneath the log,

.init replaces braces in my bog,
I hop from u256 to float so bold,
Tidy numbers, tests that hold,
A tiny hop for code, a carrot for soul 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: deprecating allocator initialization and fixing an LLVM crash in tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/utils/units.zig (1)

51-92: Consider adding edge-case tests for the new conversion helpers.

The existing tests cover typical cases well. Consider adding tests for:

  • Very large u256 values that exercise the hi/lo split path (values > 2^128)
  • Boundary precision cases
🧪 Example additional test cases
test "u256ToF64 large value with hi bits" {
    // Value with both hi and lo components: 2^128 + 1
    const large: u256 = (`@as`(u256, 1) << 128) | 1;
    const result = u256ToF64(large);
    // Should be approximately 2^128 (lo component may be lost to f64 precision)
    try std.testing.expect(result >= TWO_POW_128_F64);
}

test "parseEther very small fractional" {
    // 1 wei = 10^-18 ether
    const one_wei = parseEther(1e-18);
    // Due to f64 precision limits, this may not be exactly 1
    try std.testing.expect(one_wei <= 2);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/units.zig` around lines 51 - 92, Add edge-case tests to exercise
hi/lo u256 conversion and boundary precision: create a test that constructs a
u256 with high bits set (e.g., (`@as`(u256,1) << 128) | 1) and assert
u256ToF64(large) behaves (e.g., result >= TWO_POW_128_F64) to verify the
hi-path; add tests for tiny fractional values via parseEther(1e-18) and
parseGwei with values near precision limits to assert reasonable
behaviour/bounds (not exact equality), and add a large-ether parse test (value >
2^128) to ensure parseEther/parseGwei and roundtrip with formatEther/formatGwei
handle hi/lo splits; reference tests by name (e.g., add "u256ToF64 large value
with hi bits", "parseEther very small fractional") and reuse existing helpers
u256ToF64, parseEther, parseGwei, formatEther, formatGwei.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils/units.zig`:
- Around line 22-28: Validate the f64 before converting in f64ToU256Trunc: at
the top of f64ToU256Trunc check that value >= 0.0 and that it's finite (use
std.math.isFinite or equivalent and std.math.isnan checks) and if the check
fails, abort with a clear `@panic` or return an error (choose panic if you want no
API change), so `@intFromFloat` is never called on negative/NaN/inf; alternatively
move the same validation into the public parseEther/parseGwei functions if you
prefer to return a caller-facing error union instead of panicking.

---

Nitpick comments:
In `@src/utils/units.zig`:
- Around line 51-92: Add edge-case tests to exercise hi/lo u256 conversion and
boundary precision: create a test that constructs a u256 with high bits set
(e.g., (`@as`(u256,1) << 128) | 1) and assert u256ToF64(large) behaves (e.g.,
result >= TWO_POW_128_F64) to verify the hi-path; add tests for tiny fractional
values via parseEther(1e-18) and parseGwei with values near precision limits to
assert reasonable behaviour/bounds (not exact equality), and add a large-ether
parse test (value > 2^128) to ensure parseEther/parseGwei and roundtrip with
formatEther/formatGwei handle hi/lo splits; reference tests by name (e.g., add
"u256ToF64 large value with hi bits", "parseEther very small fractional") and
reuse existing helpers u256ToF64, parseEther, parseGwei, formatEther,
formatGwei.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4f225f and dc05cad.

📒 Files selected for processing (5)
  • docs/content/docs/introduction.mdx
  • examples/02_check_balance.zig
  • examples/04_send_transaction.zig
  • examples/05_read_erc20.zig
  • src/utils/units.zig

@koko1123
Copy link
Contributor

Thanks so much for the PR!

@vercel
Copy link

vercel bot commented Feb 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eth-zig Ready Ready Preview, Comment Feb 27, 2026 3:53pm

@koko1123
Copy link
Contributor

Investigation: LLVM crash reproduction and alternative fix

Hey @Mario-SO, thanks for the PR! I investigated this in depth and wanted to share findings.

Root cause

This is a known upstream LLVM bug: ziglang/zig#18820. LLVM cannot lower @intFromFloat/@floatFromInt for integer types >128 bits on aarch64. The crash triggers with the error:

LLVM ERROR: Unsupported library call operation!

Key finding: The crash happens on Zig 0.15.2 when running zig test src/utils/units.zig directly, but NOT when running zig build test (which goes through the build system). This is likely why CI passes on macos-latest -- it runs zig build test. The crash also reproduces on Zig 0.16.0-dev (LLVM 21.1) in both modes.

Reproduction

Minimal reproducer on aarch64:

const std = @import("std");

const ETHER: u256 = 1_000_000_000_000_000_000;

fn parseEther(ether: f64) u256 {
    const wei_f = ether * @as(f64, @floatFromInt(ETHER));
    return @intFromFloat(wei_f);
}

test "parse ether" {
    const x = parseEther(1.0);
    try std.testing.expect(x > 0);
}
# Crashes on aarch64-macos with Zig 0.15.2 and 0.16.0-dev
zig test /tmp/repro.zig
# LLVM ERROR: Unsupported library call operation!

Proposed alternative fix

The current hi/lo u128 decomposition in this PR adds intermediate arithmetic that isn't necessary. A simpler approach: route conversions through u128 as a direct intermediate, since LLVM supports f64<->u128 on all backends and realistic wei values always fit in u128 (max ETH supply is ~1.2e26 wei, u128 max is ~3.4e38):

const std = @import("std");

pub const ETHER: u256 = 1_000_000_000_000_000_000;
pub const GWEI: u256 = 1_000_000_000;
pub const WEI: u256 = 1;

// Comptime-evaluated f64 constants. Avoids runtime @floatFromInt on u256
// which crashes LLVM on aarch64 (https://github.com/ziglang/zig/issues/18820).
const ETHER_F64: f64 = @as(f64, @floatFromInt(ETHER));
const GWEI_F64: f64 = @as(f64, @floatFromInt(GWEI));

/// f64 -> u256 via u128 intermediate. LLVM supports f64<->u128 on all
/// backends; direct f64<->u256 crashes on aarch64 with LLVM 21+.
inline fn f64ToU256(val: f64) u256 {
    return @as(u256, @as(u128, @intFromFloat(val)));
}

/// u256 -> f64 via u128 intermediate. Values <= u128 max use a single
/// @floatFromInt; larger values decompose into hi/lo halves.
inline fn u256ToF64(val: u256) f64 {
    const hi: u128 = @truncate(val >> 128);
    if (hi == 0) {
        return @as(f64, @floatFromInt(@as(u128, @truncate(val))));
    }
    const lo: u128 = @truncate(val);
    return @as(f64, @floatFromInt(hi)) * (2.0 * @as(f64, @floatFromInt(@as(u128, 1) << 127))) + @as(f64, @floatFromInt(lo));
}

pub fn parseEther(ether: f64) u256 {
    return f64ToU256(ether * ETHER_F64);
}

pub fn parseGwei(gwei: f64) u256 {
    return f64ToU256(gwei * GWEI_F64);
}

pub fn formatEther(wei: u256) f64 {
    return u256ToF64(wei) / ETHER_F64;
}

pub fn formatGwei(wei: u256) f64 {
    return u256ToF64(wei) / GWEI_F64;
}

Why this is better:

  • f64ToU256 is a single fcvtzu + zero-extend (compiles to ~2 instructions)
  • u256ToF64 fast-paths through u128 with no decomposition for values < 2^128 (all realistic ETH amounts)
  • inline fn ensures no function call overhead
  • Comptime constants (ETHER_F64, GWEI_F64) avoid the crash for the constant conversions entirely
  • The hi != 0 branch for very large u256 values is kept for correctness but never taken in practice

Test fix: The "parseEther large value" test needs adjustment -- 9007.0 * 1e18 exceeds f64 mantissa precision, so exact equality isn't valid across all LLVM backends. Replace with:

test "parseEther large value" {
    // 9007.0 is exact in f64, but 9007.0 * 1e18 exceeds f64 mantissa precision.
    // Allow up to 1 ULP of error at this magnitude (~2^20 = 1048576).
    const result = parseEther(9007.0);
    const expected: u256 = 9007_000_000_000_000_000_000;
    const diff = if (result > expected) result - expected else expected - result;
    try std.testing.expect(diff < 1_048_576);
}

Verified locally (aarch64-macos, Zig 0.15.2):

  • zig test src/utils/units.zig -- 10/10 pass (original crashes with LLVM ERROR)
  • zig build test -- all pass
  • zig build bench -- no regression on any of the 26 benchmarks (none of them touch units.zig)

GPA init changes

The .init style changes to examples/docs are good and should be kept regardless.

Next steps

We're filing this as an LLVM regression on the Zig issue tracker with the minimal reproducer. Would you be open to updating this PR with the simpler u128 intermediate approach?

@koko1123 koko1123 mentioned this pull request Feb 27, 2026
@Mario-SO
Copy link
Contributor Author

wow ok, nice job going deeper here, ok will do your suggestions, it'd be nice if you linked me or this PR in the filing of the zig issue, so there is full history there.

🫡

@koko1123
Copy link
Contributor

koko1123 commented Feb 27, 2026

wow ok, nice job going deeper here, ok will do your suggestions, it'd be nice if you linked me or this PR in the filing of the zig issue, so there is full history there.

will do! you are already linked in the LLVM issue :)

@Mario-SO
Copy link
Contributor Author

Screenshot 2026-02-27 at 16 42 05

Copy link
Contributor

@koko1123 koko1123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

tagged you in the LLVM issue: llvm/llvm-project#183747

@koko1123 koko1123 merged commit 9b1e91f into StrobeLabs:main Feb 27, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants