Skip to content

Releases: SkunkWerkx/HyperCast

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 25 Oct 23:48

The first stable release. Every package's public API and the platforms in the README's
Platform support table are now a commitment under Semantic
Versioning: breaking or dropping either takes a new major version. To get there, no Swift
door throws (bad data is the verdict's Fault, as in every binding), Python and Ruby build
every number format the core accepts, the Rust scalar types hold at every boundary, what
was deprecated or never used goes (Java's GraalWasm backend, Go's ErrNativeUnavailable,
Swift's NativeLibraryError, Python's BACKEND), and every shipped file verifies from the
release tag alone.

Changed

  • Swift — no door throws, and neither does Cast.nativeVersion(). Every door and
    nativeVersion() declared throws and never threw: bad data is the Verdict's Fault,
    and the core is linked in, so there is nothing to fail to load. Call sites need no try;
    one left in place compiles with a warning. (Swift)
  • C# — HyperCast.targets no longer lists an EmccExportedFunction per entry point. The
    NativeFileReference is the whole Blazor WebAssembly wiring: the SDK's generated pinvoke
    table calls each native function by symbol, so the link resolves them against the archive
    and LibraryImport("*") reaches them through that table. The smoke test calls all twenty-seven
    entry points with the list gone (on SDK 11.0.100-rc.2) and fails at the link with the
    archive unlinked, so the list was dead weight. (C#)
  • The release is two acts at one tag, and every shipped file verifies from the tag.
    Pushing vX.Y.Z packs every registry input, attests each package and the release as a
    whole, preserves the bytes and plans every upload without publishing; a dispatch of
    release.yml with prepared_run_id publishes from the preserved bytes through what was
    the recovery path. The release attestation is one statement signed at the tag whose
    subjects are every registry package and every native library committed for Go, Swift and
    PHP, and whose predicate is the release inventory, so
    gh attestation verify <file> --repo SkunkWerkx/HyperCast --source-ref refs/tags/vX.Y.Z --predicate-type https://github.com/SkunkWerkx/.github/predicates/release/v1 answers for
    any of them, including the wheels and the committed natives that build provenance alone
    tied to the preparation commit on master (SkunkWerkx/HyperUuid#35). The GitHub Release notes come from this
    file's section for the version plus the verification commands, and a tag whose CHANGELOG
    lacks the section is refused before anything publishes. The job bodies of release.yml,
    prepare-release.yml, stage-native-binaries.yml and every static check in ci.yml
    moved to the Forge (SkunkWerkx/.github, called at master, composite actions where the
    repository's own identity signs or publishes), with this repository described to it by
    .github/hyper.json and .github/scripts/abi_probe.c. Dependabot bumps every ecosystem in
    one monthly pull request, every third-party action is pinned to a commit, and CodeQL runs
    weekly. (repository only)
  • The Go, Swift and PHP suites check that the committed core is current. Those three
    link binaries committed under go/staticlib, swift/HyperCastCore.artifactbundle and
    php/src/native, refreshed only at release, so a plain local run after a core change
    tested the core as it was at the last staging. Outside CI, a run without the local core
    (hypercast_local, HYPERCAST_LOCAL_CORE, HYPERCAST_NATIVE_LIBRARY) now stops with the
    command to use when this checkout's rust/ differs from the commit those binaries were
    built from: Go and PHP before any test, Swift as a failing CoreFreshnessTests.
    .github/scripts/local-core.sh also finds the Forge at ../HyperForge. (repository only)

Fixed

  • Python — a separator, sign or percent symbol follows the core's rule: 1 to 8 UTF-8
    bytes with no ASCII digit.
    Python alone also refused anything over two characters, so
    a format every other binding builds (four no-break spaces, say) raised ValueError
    there. The docs in every binding now state the rule the core enforces, where they said
    "one or two characters". (Python)

  • Ruby — NumFormat.from_i18n fills a missing mark instead of raising.
    from_i18n({separator: ","}) raised ArgumentError, because the missing delimiter fell
    back to "," as well. A missing mark is now whichever of "," and "." the declared one
    is not, for the currency pair too, as Python's from_localeconv and PHP's
    fromLocaleconv already did. (Ruby)

  • Rust — safe scalar representation boundaries. Symbol<N> rejects strings longer
    than its one-byte length can carry, including when N exceeds 255, preserving
    UTF-8 boundaries. Decimal display handles every public scale byte without a
    panic, allocation, clamping or precision loss; its public fields and ABI remain
    unchanged. Typed integer documentation makes the existing shortest-decimal
    behavior above 2^53 explicit. (crates.io)

  • C# — the Mac Catalyst registrar note names the SDK it was observed on. The README
    said .NET 11 RC1 while global.json pins 11.0.100-rc.2. The abort on the
    trimmable-static registrar still reproduces on SDK 11.0.100-rc.2.26504.105, so the
    smoke test keeps managed-static there and the README states it in the present
    tense. (C#)

  • PHP — no API or behaviour change. The package reaches 1.0.0 as 0.9.0 left it, its
    docs stating the core's separator rule; its committed libraries are restaged from this
    release's core, and the release attestation names each one. (Packagist)

Removed

  • Java — the GraalWasm backend. The jar no longer carries the core a second time as a
    wasm32-wasip1 module, and Cast.backend(), the hypercast.backend property and the
    fall-back to the module on a platform without a native build are gone: the binding is FFM
    downcalls into the library it bundles, as the other bindings link or load theirs. The module
    served a platform nobody had asked for, and in HyperTabular, which carried the same backend,
    replaying the suite through it in interpreter mode had become the longest step of every CI
    leg. cargo wasm-module, the wasm32-wasip1 export flags in rust/.cargo/config.toml and
    the -Pwasm runs of the JMH suite and the Native Image smoke test leave with it. (Java)
  • Go — ErrNativeUnavailable. The core is linked into the binary, so nothing ever
    returned the error or panicked with it; it was kept, deprecated, only so code testing for
    it kept compiling. Available, LoadError and NativeVersion stay. (Go)
  • Swift — NativeLibraryError. The deprecated error type had no cases and was never
    thrown, since the core is linked in on every platform. Cast.isAvailable stays, always
    true, as the availability probe every binding carries. (Swift)
  • Python — hypercast.BACKEND. It was always "native": the PyO3 extension is the only
    backend, in CPython and in Pyodide alike. Ruby's HyperCast::BACKEND stays, since it
    tells the Magnus extension from Fiddle. (Python)

The full list is in CHANGELOG.md.

Verifying

Every file in this release verifies from the tag alone. One attestation, signed by this
repository's release.yml at v1.0.0, names all 102 of them: every registry
package (NuGet, RubyGems, PyPI, crates.io and Maven Central), the 34 native
libraries committed for Go, Swift and PHP, and the 8 metadata files staged beside
them. Its predicate is the release inventory attached below.

gh attestation verify <file> --repo SkunkWerkx/HyperCast \
  --source-ref refs/tags/v1.0.0 --predicate-type https://github.com/SkunkWerkx/.github/predicates/release/v1

The native libraries, the Python wheels and the archives inside the NuGet package and the
jar also carry build provenance from CI run 40232760003
at commit 07c0f9a, the parent of the tagged commit, signed by the Forge:

gh attestation verify <file> --repo SkunkWerkx/HyperCast --signer-repo SkunkWerkx/.github --source-digest 07c0f9a140eed2899c64b5783a7c0eeb6d04794b

inventory.json lists every file with its size and SHA-256 and, for each file with build
provenance, the commit it was built from and the repository that signed it; it carries
build provenance of its own at the tagged commit. PyPI additionally holds a PEP 740 publish attestation for every wheel.

v0.9.0 - the culture-fidelity sweep, free-threaded Python, and Blazor fixes

Choose a tag to compare

@buvinghausen buvinghausen released this 21 Oct 03:30
v0.9.0

0.9.0 is the culture-fidelity sweep planned in the 0.8.0 roadmap: everything a culture declares beyond its separators now crosses into the core, in one coordinated ABI change across the core and all seven bindings. One rule throughout: the ASCII or English default is always read, and a declared symbol is a further spelling read beside it, so no input that parsed in 0.8.0 reads differently. Also free-threaded Python, and the Blazor WebAssembly fixes from Norse Architecture's integration. The full list, with a bullet per binding, is in CHANGELOG.md.

Highlights

  • Every numeric format carries what a culture declares. NumFormat gains, beside the separators, flags and currency symbol it had:

    • minus_sign and plus_sign (U+2212 for Estonian and Basque; an RTL mark and a hyphen).
    • currency_decimal_sep and currency_group_sep, read in place of the number separators once the declared currency symbol is found, as the BCL switches when it sees the symbol (en-DE writes 1.234,5 and €1,234.50).
    • percent (٪, %) and digits, the zero of a native digit block read beside ASCII (١٢٣, 123).
    • A separator may now be one or two characters.
    • A new lenience, TRAILING_SIGN, reads the sign after the digits (5-, 5 €-, 2.5%-). It is not part of ALL; every bridge sets it from the culture's own negative pattern.

    corpus/integer.json, real.json and decimal.json pin every row, and every suite replays them.

  • Day-period designators beside the declared date order. The datetime door reads a culture's own AM/PM words (오전/오후, vorm./nachm., a. m./p. m.) after the clock or, as Korean and Chinese write them, ahead of it, beside the English pair. The C ABI's cast_datetime takes a RawDayPeriods pointer after the order, null for English only. corpus/datetime.json pins it.

  • Every binding takes the whole declaration, and every bridge fills every field its platform has.

    Language Numeric format Day periods Bridge
    Rust with_signs, with_currency_separators, with_percent, with_digits; Symbol<8> separators cast_datetime_with(text, order, &DayPeriods) —
    C# init-only MinusSign, PlusSign, CurrencyDecimalSeparator, CurrencyGroupSeparator, PercentSymbol, DigitZero (Rune?); NumStyles.TrailingSign DateFormat(order, am, pm); Cast.DateTime overloads for DateFormat and IFormatProvider? NumFormat.From and DateFormat.From read every field from NumberFormatInfo and DateTimeFormatInfo; neither throws on any runtime's ICU or NLS data
    Java with* for each field; STYLE_TRAILING_SIGN DateTimeFormat(order, am, pm); Cast.dateTime overloads for DateTimeFormat and Locale NumFormat.from(Locale) and new from(DecimalFormat); `Date
    Swift minusSign, plusSign, currencyDecimalSeparator, currencyGroupSeparator, percentSymbol, digitZero; .trailingSign DateFormat(order:amSymbol:pmSymbol:); dateTime(_:format:) and dateTime(_:locale:) NumFormat.from(locale:) and new from(formatter:); DateFormat.from(locale:)
    PHP Named constructor arguments; NumFormat::TRAILING_SIGN new DayPeriods($am, $pm) as the third argument of Cast::datetime fromLocaleconv and fromNumberFormatter read the monetary separators, signs, percent, zero digit and negative suffix
    Ruby Keywords on the Data; HyperCast::TRAILING_SIGN HyperCast.datetime(text, order, am:, pm:) NumFormat.from_i18n reads negative_format and takes currency_format:
    Python Keyword-only constructor arguments; NumFormat.TRAILING_SIGN cast_datetime(text, order, am=, pm=) NumFormat.from_localeconv reads the monetary separators and signs

    The boolean door stays English, by decision: no platform's culture data carries a boolean lexicon to bridge. A feed that says ja/oui maps its words before the door.

  • The format crosses the C ABI as a new, size-tagged layout. RawNumFormat is a size word followed by NumFormat itself, now #[repr(C)]: 92 bytes, read by the core in place, so a cast pays for a check and no copy. RawDayPeriods is 40 bytes. rust/src/abi.rs has the offset table, and every binding restates it and pins it in its interop tests. Measured through the C ABI, a grouped cast_i32 under a declared $ went from 21.9 ns on the 0.8.0 core to 19.6 ns: the sweep costs nothing at the crossing. In Rust, decimal_sep and group_sep are Symbol<8> where they were char; NumFormat::new still takes chars.

  • Python: free-threaded CPython, with the GIL off. Each platform gets three wheels where it had one: cp311-abi3 for CPython 3.11+ with the GIL as before, cp314-cp314t for free-threaded 3.14, and cp315-abi3.abi3t, PEP 803's stable ABI for 3.15+ with or without the GIL. The module declares it needs no GIL, so a free-threaded interpreter keeps it off after import hypercast, and every door is safe to call from many threads at once. PyPI gets 25 wheels per release where it got 9. A build from a checkout needs maturin 1.15 or newer, and the crate's python-values feature is now built on PyO3 0.30.

  • C#: Blazor WebAssembly. A publish without the wasm-tools workload used to skip linking the native core and ship a runtime with no cast_* symbols, with nothing louder than the SDK's own warning. The package's targets now fail the build with HYPERCAST002; an app that gates on Cast.IsAvailable and keeps a managed fallback sets HyperCastRequireWasmWorkload to false. The browser-wasm archive moves to runtimes/browser-wasm/nativeassets/net11.0/, matching the only framework that links it, and it now carries build provenance, which it did not in 0.8.0.

  • Benchmark rows for a declared currency symbol and for rejection, in every binding. A declared symbol costs between nothing measurable (Python, PHP) and 11 ns (Go). A malformed value is faster than the platform's own failing parse wherever that parse throws: 4.6x in Python, 16x in Java.

Fixed

  • C#: hyper-pack-nuget.yml now verifies every native it packs and fails the release on one without provenance, which is how the unattested browser-wasm archive in 0.8.0 went unnoticed.
  • C# README: the generic ParseRequired<T> example did not compile (CS8780). It now uses the TryGetValue chain.
  • **Pffi.enable=preload the first numeric door failed with "FFI API is restricted". It now preloads both directories.

Upgrading

  • Rust: version = "0.9" in Cargo.toml. A crate using python-values moves to PyO3 0.30 with it; Cargo refuses two PyO3 versions in one build.
  • Every binding: NumFormat's separator fields are strings where they were a single character.
  • PHP: NumFormat::codePoints() is removed; packed() returns the 92-byte layout. The per-call fromNumberFormatter memo now reads twelve attributes and costs about four times a hoisted NumFormat, so hoist it.
  • Swift: from(locale:) derives afresh for .current and .autoupdatingCurrent so a user's overrides are honoured; a hot loop passes an explicit locale or a held NumFormat.
  • C#: the browser-wasm archive's path moved to net11.0. The package's targets name it, so nothing a consumer sets changes.

Verifying

Every native library and static archive in this release is built by CI and carries a build-provenance attestation, the browser-wasm archive now included:

gh attestation verify runtimes/browser-wasm/nativeassets/net11.0/libhypercast.a \
  --repo SkunkWerkx/HyperCast --signer-repo SkunkWerkx/.github

The gems and Python wheels are attested by this repo's own release.yml, so --repo SkunkWerkx/HyperCast alone verifies them.

v0.8.0 - a char door, one generic door, the platform's own format, and Android

Choose a tag to compare

@buvinghausen buvinghausen released this 09 Oct 04:16

0.8.0 comes from Norse Architecture, which is replacing its last hand-written scalar parsers with HyperCast and filed the two issues behind the first two highlights. It also adds one new platform family: C#, Go and Swift now reach Android. Everything is additive, and no input gets a different verdict than it did in 0.7.0. The full list is in CHANGELOG.md.

Highlights

  • A char door, in the core and every binding (#22). It reads exactly one character verbatim, checked before trimming, so " " is a space and "6" the digit six. Otherwise it reads one declared code point: decimal 65, U+0041, 0x41, &H41, or an HTML entity &#65; / &#x41;. A surrogate or anything past U+10FFFF is OutOfRange. It is the twenty-sixth C ABI export, cast_char, and corpus/char.json is replayed by all eight suites.

    Language Door Returns
    Rust cast_char char
    C# Cast.Char char
    Java Cast.character Character
    Go Char rune
    Swift Cast.char Unicode.Scalar
    PHP, Ruby, Python Cast::char, HyperCast.char, cast_char a one-character string

    C# and Java return a scalar past U+FFFF as OutOfRange, since their char is one UTF-16 unit.

  • One door generic over every target (#21), in C#, Go and Swift. It picks the door from the requested type, so a caller holding a generic T routes through HyperCast in one call.

    • C#: Cast.Scalar<T> over bool, the numerics, Guid, DateOnly, TimeOnly, DateTimeOffset, TimeSpan, char and DateTime. Scalar<DateTime> reads RFC 3339 as UtcDateTime.
    • Go: Scalar[V ScalarTarget, T Text]. A type outside the union is a compile error.
    • Swift: Cast.scalar<T: ScalarCastTarget>(_:format:).
  • The numeric doors take the platform's own format object. A caller holding a culture or locale passes it straight through instead of building a NumFormat first. Each maps to the core's format with every lenience on, and a NumFormat is still how to declare anything stricter.

    • C#: an IFormatProvider? overload of every numeric door, Numeric<T> and Scalar<T>. null is the current culture, as in TryParse.
    • Java: a Locale overload of every numeric door. NumFormat.from(Locale) is now cached per locale (52.6 ns against 437.6 uncached).
    • Swift: a locale: overload of every numeric door, numeric and scalar.
    • PHP: every numeric door accepts NumFormat|\NumberFormatter, plus NumFormat::fromNumberFormatter. intl stays optional.
    • Ruby: Rails' separator:, delimiter: and unit: keywords on every numeric door, plus NumFormat.from_i18n for an I18n.t("number.format") Hash.
    • Python and Go keep taking a NumFormat, since neither standard library has a per-object locale to take.

    Every bridge now documents what a culture carries that the core's format cannot yet: a minus sign other than ASCII -, currency separators that differ from the number ones, and a percent symbol other than %. The roadmap plans the round that closes those gaps.

  • Android, in C#, Go and Swift, on arm64 and x86_64.

    Language How
    C# The package carries android-arm64 and android-x64, so a .NET MAUI app has the core on every platform MAUI targets with nothing but the package reference. CoreCLR loads libhypercast.so from the APK, and a Native AOT publish links it in from a static archive. The libraries are NDK-built for API 21 and 16 KB-aligned, as Android 15 devices and Google Play require.
    Go GOOS=android links go/staticlib/android_{arm64,amd64}, with the NDK's clang as CC.
    Swift Links the artifact bundle's Android variants with the Swift SDK for Android (Swift 6.3+, API 28+).

    CI builds the C# app both ways for both RIDs from a package packed in the same run, cross-builds the Go and Swift suites, and runs everything x86_64 in an emulator with 16 KB pages.

Fixed

  • Ruby: the native extension survives a compacting garbage collection. It kept a handful of Ruby objects (Success, Fault, Date, DateTime, Decimal and the two built-in formats) in a Rust static that a compacting GC could move. A door called after a compaction could then segfault. Each is now pinned when the extension loads, and a new spec compacts before calling a door.
  • Go: a build with no cgo (CGO_ENABLED=0) once again fails with only the one explanatory error, instead of also reporting undefined: symChar.

Upgrade notes

Source-compatible for every consumer except one narrow case.

  • Java: a call that passed a literal null as the format, such as Cast.i32(text, null), no longer compiles, because the new Locale overload makes it ambiguous. It already threw at run time, so no working code depends on it.
  • Rust: version = "0.8" in Cargo.toml. Under 0.x caret rules, "0.7" never resolves to 0.8.

Verifying

Every native library and static archive in this release is built by CI and carries a build-provenance attestation, the new Android libraries and archives included:

gh attestation verify go/staticlib/android_arm64/libhypercast.a \
  --repo SkunkWerkx/HyperCast --signer-repo SkunkWerkx/.github

The gems and Python wheels are attested by this repo's own release.yml, so --repo SkunkWerkx/HyperCast alone verifies them.

The CHANGELOG.md and roadmap links point at the v0.8.0 tag, so they'll only resolve once you've pushed it. If staging ends up on a different date, change the 2026-10-08 in the CHANGELOG heading to match.

C# and Java return a scalar past U+FFFF as OutOfRange, since their char is one UTF-16 unit.

  • One door generic over every target (#21), in C#, Go and Swift. It picks the door from the requested type, so a caller holding a generic T routes through HyperCast in one call.

    • C#: Cast.Scalar<T> over bool, the numerics, Guid, DateOnly, TimeOnly, DateTimeOffset, TimeSpan, char and DateTime. Scalar<DateTime> reads RFC 3339 as UtcDateTime.
    • Go: Scalar[V ScalarTarget, T Text]. A type outside the union is a compile error.
    • Swift: Cast.scalar<T: ScalarCastTarget>(_:format:).
  • The numeric doors take the platform's own format object. A caller holding a culture or locale passes it straight through instead of building a NumFormat first. Each maps to the core's format with every lenience on, and a NumFormat is still how to declare anything stricter.

    • C#: an IFormatProvider? overload of every numeric door, Numeric<T> and Scalar<T>. null is the current culture, as in TryParse.
    • Java: a Locale overload of every numeric door. NumFormat.from(Locale) is now cached per locale (52.6 ns against 437.6 uncached).
    • Swift: a locale: overload of every numeric door, numeric and scalar.
    • PHP: every numeric door accepts NumFormat|\NumberFormatter, plus NumFormat::fromNumberFormatter. intl stays optional.
    • Ruby: Rails' separator:, delimiter: and unit: keywords on every numeric door, plus NumFormat.from_i18n for an I18n.t("number.format") Hash.
    • Python and Go keep taking a NumFormat, since neither standard library has a per-object locale to take.

    Every bridge now documents what a culture carries that the core's format cannot yet: a minus sign other than ASCII -, currency separators that differ from the number ones, and a percent symbol other than %. The roadmap plans the round that closes those gaps.

  • Android, in C#, Go and Swift, on arm64 and x86_64.

    Language How
    C# The package carries android-arm64 and android-x64, so a .NET MAUI app has the core on every platform MAUI targets with nothing but the package reference. CoreCLR loads libhypercast.so from the APK, and a Native AOT publish links it in from a static archive. The libraries are NDK-built for API 21 and 16 KB-aligned, as Android 15 devices and Google Play require.
    Go GOOS=android links go/staticlib/android_{arm64,amd64}, with the NDK's clang as CC.
    Swift Links the artifact bundle's Android variants with the Swift SDK for Android (Swift 6.3+, API 28+).

    CI builds the C# app both ways for both RIDs from a package packed in the same run, cross-builds the Go and Swift suites, and runs everything x86_64 in an emulator with 16 KB pages.

Fixed

  • Ruby: the native extension survives a compacting garbage collection. It kept a handful of Ruby objects (Success, Fault, Date, DateTime, Decimal and the two built-in formats) in a Rust static that a compacting GC could move. A door called after a compaction could then segfault. Each is now pinned when the extension loads, and a new spec compacts before calling a door.
  • Go: a build with no cgo (CGO_ENABLED=0) once again fails with only the one explanatory error, instead of also reporting undefined: symChar.

Upgrade notes

Source-compatible for every consumer except one narrow case.

  • Java: a call that passed a literal null as the format, such as Cast.i32(text, null), no longer compiles, because the new Locale overload makes it ambiguous. It already threw at run time, so no working code depends on it.

Fixed

  • Ruby: the native extension survives a compacting garbage collection. It kept a handful of Ruby objects (Success, Fault, Date, DateTime, Decimal and the two built-in formats) in a Rust static that a compacting GC co...
Read more

v0.7.0 - interop, numbers already held, faster, and iOS

Choose a tag to compare

@buvinghausen buvinghausen released this 07 Oct 03:07
03aec55

0.7.0 is shaped by the libraries that build on HyperCast: HyperTabular and HyperWorkbook. It also adds one new platform family: C#, Swift and Go now link the core into iOS and Mac Catalyst apps, as HyperUuid's 0.7.0 does. The manifests read 0.6.2 between releases, but no 0.6.2 was ever published, so everything since 0.6.1 is here. The full list is in CHANGELOG.md.

Highlights

  • iOS and Mac Catalyst, in C#, Swift and Go. Those platforms load no libraries, so the core is linked into the app from static archives built and attested in the same CI job as everything else.

    Language How
    C# A .NET iOS, MAUI or Mac Catalyst app (net11.0-ios, net11.0-maccatalyst) references the package and writes nothing else. It covers ios-arm64, iossimulator-arm64, maccatalyst-arm64 and maccatalyst-x64.
    Swift iOS 16, the iOS simulator and Mac Catalyst 16 on arm64, linked from a new HyperCastCoreApple.xcframework. Linux, Windows and WebAssembly builds see the same package as before.
    Go cgo builds for an iOS device, the iOS simulator on Apple silicon (-tags iossimulator) and Mac Catalyst on either architecture (-tags maccatalyst, which gomobile sets).

    CI tests each one on a Mac on every PR: the suites run in an iOS simulator and as a Mac Catalyst process, and builds for a real iOS device are checked to contain the core. Android, tvOS and the Intel iOS simulator remain unsupported.

  • Interop: a public surface for libraries that carry HyperCast's results across a C ABI of their own. Every binding now exposes the code that turns the core's raw out-values, numeric format, reason codes and fault spans into its own types. The doors use exactly that code, so a value read out of another library's buffer is the value the door would have returned. HyperTabular no longer copies private code.

    • C#: HyperCast.Interop
    • Java: io.github.skunkwerkx.hypercast.interop
    • Swift: Interop
    • PHP: HyperCast\Interop
    • Ruby: HyperCast::Interop
    • Go: the Raw* types and *FromCode functions
    • Python: NumFormat.packed
    • Rust: from_code on the enums, a public RawNumFormat, and a python-values feature
    • Rust's C symbols are now an exports feature, so a crate that exports its own C ABI can turn them off.
  • Numbers already held. A workbook stores a numeric cell as a double, not text. Four new doors read that f64 directly as:

    • an exact decimal
    • an Excel serial
    • a time of day
    • a span

    That takes the C ABI to 25 cast_* exports plus hypercast_version, and every binding has them. Rust also gains the whole typed family (i32_from_f64 … excel_duration).

    • The double is read as the number it names: 2.5 is 2.5, the digits Excel and LibreOffice wrote into the file.
    • Times read back exactly: Excel's and Google's serials snap to the time the writer stored, instead of drifting into float noise.
  • Faster. Rust figures on linux-x64:

    • The temporal doors are about twice as fast: cast_timestamp 26.6 → 14.5 ns, cast_datetime 29.3 → 13.9 ns, cast_duration 35.5 → 23.6 ns.
    • cast_f64 and cast_f32 are 22-36% faster.
    • cast_uuid is about 20% faster.

    Bindings that cross cheaply keep most of it. In Swift, duration went from 45 to 33 ns. C#'s Cast.Duration went from 49.1 to 31.6 ns. The shared library grows about 10 KB for the inlining.

  • Every door is now proven unable to panic. cast_f32 and cast_f64 were the last two outside the link-time proof. Their float conversion is now HyperCast's own code and gives the same bits as core's parser, checked over about 1.5 million generated inputs. All 26 exports are in the proof on every PR.

Upgrade notes

Source-compatible for almost every consumer. The first item is the only one most will meet.

  • Every package: a fraction of a second past nine digits now succeeds, truncated to the nanosecond. 15:04:05.0000000001 was Malformed. ISO 8601, RFC 3339 and XSD put no cap on fraction digits, and Excel writes seventeen. Code that relied on the refusal must check the text itself.
  • Rust: a default-features = false build no longer carries the C ABI symbols. Name exports beside std if you relied on them. Default builds are unchanged.
  • PHP: HyperCast\NativePlatform (always @internal) is now HyperCast\Interop\NativePlatform, and its methods take the library's base name first.
  • Java: CastFailure.fromCode is public and returns Optional<CastFailure> instead of throwing. It was package-private before.
  • Go: GOOS=android no longer compiles. It used to link the untested Linux archive. If you need Android, open an issue.

Verifying

Every native library and static archive in this release is built by CI and carries a build-provenance attestation, the new iOS and Mac Catalyst archives included:

gh attestation verify go/staticlib/linux_amd64/libhypercast.a \
  --repo SkunkWerkx/HyperCast --signer-repo SkunkWerkx/.github

The gems and Python wheels are attested by this repo's own release.yml, so --repo SkunkWerkx/HyperCast alone verifies them

v0.6.1 - Ruby in the browser

Choose a tag to compare

@buvinghausen buvinghausen released this 03 Oct 20:31
c355f35

0.6.1 adds Ruby to the browser languages: HyperCast now runs in a web page from C#, Rust, Python, Go, Swift and Ruby. It also fixes four papercuts from 0.6.0 in Rust, C#, Go and the Ruby extension. The full list is in CHANGELOG.md.

Highlights

  • Ruby in the browser: the new hypercast-wasm gem. ruby.wasm links extensions into the interpreter when rbwasm build makes it. So in the Gemfile you hand to rbwasm build, list hypercast-wasm instead of hypercast:

    gem "hypercast-wasm"
    gem "js"
    gem "bigdecimal" # only for Decimal#to_d
    • Same extension: you get the same Magnus extension as the platform gems, prebuilt for wasm32-wasip1, so HyperCast::BACKEND is :native and every door works as on a host Ruby.
    • No Rust toolchain: the gem carries one archive for Ruby 3.4 and one for 4.0, and --ruby-version picks.
    • Shares an interpreter with HyperUuid: hyperuuid-wasm links into the same interpreter.
    • Tested in CI: CI builds each archive from the commit, attests it, and runs the gem linked into a fresh interpreter under Node and in headless Chrome.

    See Ruby in the browser.

    This was listed under "Proven, not shipped" in 0.6.0. @matsadler fixed both Magnus bugs that held it back (magnus#186, #187) and shipped them in Magnus 0.8.3 and 0.9.2 the day after they were filed. That's what made this release possible. Thank you, Mat!

Fixes

  • Ruby: the Magnus extension compiles on 32-bit targets. It held raw VALUEs as u64, which only type-checks where VALUE is 64 bits; the first wasm32-wasip1 build caught it. Nothing changes on the platform gems' targets.
  • Rust: the no-panic proof from a consumer's crate now passes with lto = true alone, as the README says. cast_uuid used to keep a bounds check that depended on the optimizer; it now reads every byte with get. Verdicts and speed are unchanged.
  • C#: a .NET 10 project is refused at restore with NuGet's NU1202, instead of restoring and then failing to compile on missing HyperCast types. The targets file now ships under build/net11.0/ and buildTransitive/net11.0/. Nothing changes for .NET 11, Native AOT or Blazor WebAssembly.
  • Go: printing a nil *Fault (a door's success) no longer traps under TinyGo. Error returns <nil> for a nil receiver itself.

Upgrade notes

Drop-in for every binding. The Ruby extension now builds on Magnus 0.9.2 (from 0.8.2), with no API change. On .NET 10 the package is now refused at restore rather than at compile, and it never worked there.

Verifying

Every native library, static archive and wasm32-wasip1 archive in this release is built by CI and carries a build-provenance attestation:

gh attestation verify go/staticlib/linux_amd64/libhypercast.a \
  --repo SkunkWerkx/HyperCast --signer-repo SkunkWerkx/.github

The gems, hypercast-wasm included, are attested by this repo's own release.yml, so --repo SkunkWerkx/HyperCast alone verifies them. The Python wheels verify the same way, or with --owner SkunkWerkx.

v0.6.0 - one way in, in the browser, and impossible dates are out of range

Choose a tag to compare

@buvinghausen buvinghausen released this 03 Oct 04:22

0.6.0 brings HyperCast level with HyperUuid's 0.6.0. Every binding reaches the core one way, HyperCast runs in the browser for C#, Rust, Python, Go and Swift, and every package is smaller. There was no HyperCast 0.5.0, so this release also carries that round's work. The full list is in CHANGELOG.md.

Highlights

  • In the browser, proven in a browser. Every route below runs in headless Chrome in CI, on every PR.

    Language How
    C# Blazor WebAssembly (.NET 11+): a plain PackageReference, as before
    Rust wasm32-unknown-unknown, no extra setup
    Python (new) Pyodide: await micropip.install("hypercast") finds a new pyemscripten wheel on PyPI (~150 KB). The whole test suite runs inside Pyodide in CI.
    Go (new) TinyGo 0.42+ with -target=wasm, the core linked in
    Swift (new) the wasm32-wasip1 build, run in a page through a WASI shim
  • Go links the core, and that's the only way. cgo statically links the core on Linux, macOS and now Windows, using the same MSVC archive C# links under Native AOT.

    • Removed: purego, the loading cgo backend, the wasmtime backend, and the nine embedded libraries under go/native/.
  • Swift links the core on every platform. macOS and Windows join Linux and WebAssembly: no loader, no resource bundle, nothing to deploy beside your executable, and no door that can throw.

  • Ruby runs Magnus on Alpine.

    • New musl platform gems carry Magnus extensions for Ruby 3.4 and 4.0.
    • Fiddle is now only the universal gem's last resort.
    • Platform gems no longer carry Fiddle libraries.
  • Smaller everywhere.

    • The native libraries are built without Rust's standard library, and stripped. The linux-x64 library is 104,496 bytes, from 529,960 in 0.4.0.
    • Go's module and the Composer package drop their embedded libraries, and the Composer archive no longer carries the other bindings.
    • A Blazor app no longer copies a 3 MB archive into its output.
  • Proven, every PR.

    • Panics: every door except cast_f32/cast_f64 is proved at link time unable to panic. The two float doors' remaining paths are inside core's float parser.
    • Correctness: a round-trip suite checks about 34,000 one-character edits against a naive reference parser.
    • Rust: the crate declares rust-version = "1.88", with clippy, semver and MSRV checks.

Upgrade notes

Go (breaking: the reason this is 0.6.0).

  • Needs cgo and a C compiler where it's built:
    • Linux: gcc or clang (build-base on Alpine)
    • macOS: the Xcode command-line tools
    • Windows: MinGW-w64 gcc (llvm-mingw on arm64)
  • Unsupported builds fail at compile time, with an error that names the fix. That covers CGO_ENABLED=0, stock Go's GOOS=js/wasip1, and any other platform. Use TinyGo for WebAssembly.
  • Drop the old build tags -tags hypercast_dynamic and -tags hypercast_wasm.
  • API unchanged: Available() is always true, and ErrNativeUnavailable is deprecated.

Every package: impossible dates and times are OutOfRange, not Malformed.

  • Malformed now means the wrong shape: a missing separator, a stray character, three digits where two belong.
  • Text shaped right that names something nonexistent is OutOfRange. That covers month 13, 2026-02-30, 24:00, a :60 leap second, an offset past 23:59, the colon duration 25:00:00, and Excel's phantom 1900 serial 60.
  • The fault span still points at the same field. Year 0000 now points at the year instead of the whole date.
  • Shape is checked across the whole input first, so 2026-13-01T00:00:00x is Malformed at the x.
  • Code that only checks for success, or reports the span, needs nothing.

Swift.

  • Nothing to deploy any more on macOS and Windows: remove the step that copied HyperCast_HyperCast.bundle/.resources beside your executable.
  • NativeLibraryError has no cases now, so code matching .openFailed/.symbolNotFound no longer compiles.
  • Unsupported platforms fail at compile time.

Ruby.

  • Alpine moves from Fiddle to Magnus automatically.
  • The glibc platform gems are renamed x86_64-linux-gnu / aarch64-linux-gnu. RubyGems and Bundler resolve them by themselves; only an explicit --platform x86_64-linux or a pinned gem file name needs updating.
  • Intel Macs stay on Fiddle via the universal gem.

Python and Ruby.

  • The opt-in wasmtime backend is gone, along with the [wasm] extra.
  • HYPERCAST_WASM is now ignored. The native backend always loads.

C#, Java, PHP and Rust are drop-in apart from the verdict change above.

Fixes

  • Rust: default-features = false builds on every target again. Through 0.4.0 it failed with "#[panic_handler] function required" everywhere but bare metal.
  • Rust: cast_uuid now passes the no-panic proof from a consumer's crate too. Running it from your own crate needs lto = true in your release profile.
  • C#: a Blazor WebAssembly app no longer gets the 3 MB libhypercast.a in bin/ and the publish output.
  • Python: the wheels are built, installed and called on their own platform on every CI run, and the release publishes exactly those. Through 0.4.0 they were first built at the tag.

Proven, not shipped

  • PHP in the browser works: the ext-php-rs extension runs as a WordPress Playground side module (258 KB, 85 KB gzipped), and php/README.md has the recipe.
  • Ruby in the browser is proven in HyperUuid and waits on two Magnus fixes (magnus#186, magnus#187).

Verifying

Every native library and static archive in this release is built by CI and carries a build-provenance attestation:

gh attestation verify go/staticlib/linux_amd64/libhypercast.a \
  --repo SkunkWerkx/HyperCast --signer-repo SkunkWerkx/.github

The Python wheels, the Pyodide wheel included, verify the same way, or with --owner SkunkWerkx.

v0.4.0 - a fast lane for money, Alpine, and a linked-in core

Choose a tag to compare

@buvinghausen buvinghausen released this 02 Oct 03:52

0.4.0 makes grouped and currency input as cheap as it should be, widens where HyperCast runs, removes the shared library from the builds that never needed one, and replaces the published benchmarks with a fresh set measured on x86-64. No door changes its verdict or its fault span on any input. The full list is in CHANGELOG.md.

Highlights

  • Grouped, currency and accounting input casts in about half the time. 12,345.67, $12,345.67 and ($12,345.67) used to go through the full normalize-then-parse engine. A second fast lane in the core now reads them in one pass: a grouped i64 41–45 ns → 14, a decimal 42–50 ns → 21–24, a real 40–42 ns → 33. Every numeric door is tested with the lane on and off and must give identical answers.
  • Alpine works. linux-musl-x64 and linux-musl-arm64 are built, attested and shipped in every package (as musllinux_1_2 wheels for Python). Through 0.3.0 Alpine got the glibc library, which does not load under musl.
  • The core links in where it can.
    • Swift on Linux links a static library through SwiftPM, which adds the static Linux SDK (musl) and WebAssembly to that binding.
    • Go cgo builds on Linux and macOS link the core instead of embedding and dlopening it. The binary runs from a read-only or scratch image.
    • C# Native AOT publishes link the core into the executable.
  • Java in a GraalVM Native Image is fast. FFM doors were being interpreted there: 7.8–9.1 µs a door against 18–52 ns on the JVM. They are now compiled: 60–111 ns. Nothing changes on the JVM, and a consumer's native-image build needs no configuration.
  • Python doors that return an object cost about half. cast_uuid 557 → 262 ns, cast_timestamp 369 → 252 ns, cast_datetime 339 → 230 ns, cast_duration 581 → 262 ns. Wheels are unchanged (still one abi3 wheel per platform), and the package is now typed.
  • Ruby doors on the Magnus backend cost a quarter of what they did. bool 462 → 112 ns, i32 468 → 133 ns, uuid 629 → 223 ns, timestamp 794 → 447 ns.
  • The native libraries report the right version. 0.3.0's libraries say 0.2.0 (behaviour was unaffected; the number was wrong). This release is built in the corrected order.

Benchmarks, re-measured

Every table was re-measured on linux-x64 (Intel Core i9-11900H) with each binding's own harness. The previous figures came from an arm64 WSL2 machine. The doors' own costs moved little; the platform parsers they are timed against moved a lot, so several verdicts changed. Each README names the machine and runtime behind its tables.

The RFC 3339 timestamp door against each platform's own parser:

Binding HyperCast Platform parser Verdict
Swift 51 ns 642 ns ISO8601FormatStyle 12.6x faster
Java 50.7 ns 615 ns Instant.parse 12.1x faster
Ruby (Magnus) 447 ns 2.97 µs Time.iso8601 6.6x faster
C# 52.5 ns 128 ns DateTimeOffset.TryParse 2.4x faster
PHP 538 ns 560 ns DateTimeImmutable level
Python 252 ns 146 ns fromisoformat 1.7x slower
Go 77 ns 44 ns time.Parse slower

The doors win wherever the platform's parser is culture machinery, and draw or lose wherever it is a C builtin for the one shape being timed: Guid.TryParse, UUID.fromString, int(), fromisoformat. Those losses are a few nanoseconds to a few hundred, and what they buy is a format declared per call, a fault with a byte span, and the same verdict in seven languages. On messy input the gap goes the other way: 1/7/2026 3:04 PM is 20x faster than Python's strptime, 6.9x faster than a Java DateTimeFormatter, and 3.9x faster than DateTime.TryParse.

Upgrade notes

Runtime floors. Every floor that had reached end of life is raised. A consumer on an older runtime keeps resolving 0.3.0.

Floor
Python 3.11
Ruby 3.3
PHP 8.2
Java JDK 25
Swift 6.2

Behaviour changes.

  • PHP: ext-mbstring is no longer needed; ext-ffi is the only required extension.
  • PHP, Python: NumFormat::fromLocaleconv / from_localeconv never puts one character in both separator roles. A comma-decimal locale with no thousands separator now groups on .; PHP used to throw for it.
  • Ruby: caller bugs raise the same exception on every backend, and the uuid door returns a US-ASCII String on every backend.
  • Go: LoadError() says why the core did not load, and the doors panic with ErrNativeUnavailable so a recover can errors.Is it. -tags hypercast_dynamic keeps the previous loading cgo backend.
  • Swift: NativeLibraryError is public and LocalizedError.
  • C#: set HyperCastStaticLink=false to keep the shared library under Native AOT.
  • Java: the optional GraalWasm engine moves to 25.4.4.1.1. It must be the same release as the GraalVM JDK you run on.
  • Unknown architectures are no longer treated as x64. Go and PHP report an unsupported platform, Swift refuses to compile, and Java falls back to its wasm backend.
  • Intel macOS (osx-x64) still ships in every package. There is no precompiled x86_64-darwin gem; Ruby there runs on the Fiddle backend.

Fixes

  • C#: a NumFormat with no currency symbol set could throw NullReferenceException; DateOrders.From(null) now throws ArgumentNullException.
  • C#: a Blazor WebAssembly app could not use HyperUuid and HyperCast together (duplicate symbol at link time), failed in the browser on .NET 11, and got no native link when it reached the package through a class library.
  • Ruby: HyperCast.date(text, nil) raised TypeError on the Magnus backend though nil is the documented default; on Alpine, 0.3.0 loaded the glibc library and available? was false.
  • Swift: a missing resource bundle crashed the process, Cast.isAvailable included. The probe is now false and the doors throw.
  • Python: bytes input cost about a microsecond more than str on every door.
  • Java: selecting the wasm backend without GraalWasm on the classpath now says what to add.

Verifying

Every native library and static archive in this release is built by CI and carries a build-provenance attestation:

gh attestation verify go/native/linux-x64/libhypercast.so \
  --repo SkunkWerkx/HyperCast --signer-repo SkunkWerkx/.github

v0.3.0 - what the first consumer asked for

Choose a tag to compare

@buvinghausen buvinghausen released this 04 Sep 19:58
735e7e7

The first real consumer to route an entire scalar-parser family through a HyperCast binding was Svartalfheim's ingestion branch (NorseArchitecture/Svartalfheim#62). Read against this repo, that one PR surfaced every gap this release closes: a declared currency symbol, without which every non-invariant culture fell back to managed code; an exact decimal door; a way to prove the native library loaded before the first cast; and a handful of binding ergonomics. It is one coordinated ABI bump across the core and all seven bindings. docs/roadmap.md records the reasoning; CHANGELOG.md is the rdocs/roadmap.md records the reasoning; CHANGELOG.md is the rThis release also carries the wasm backends for Java, Ruby, Pythn Unreleased: the core as a wasm32-wasip1 module runningin-process through an engine each ecosystem already has, behind nd switch, opt-in and dependency-free until asked for.## The core- A declared currency symbol on NumFormat, up to 16 UTF-8 URRENCYflag that joinsALL. Accepted once at either edge ofthe numeric body — leading before or after a sign ($5, -$5, 1.234,50 kr.) — with accounting parentheses wrapping symbol anddigits together (($5)). Declared with the flag off, the symboly character; flag on with nothing declared changes nothing.- cast_decimal — the real doors' grammar to an exact sign, scale. No float is ever formed: 0.1 is one tenth and 50% isexactly 0.5. The result is canonical — trailing fraction zeros come out the same, and zero is scale 0, never negative. Nothingbut a zero is ever dropped: excess nonzero precision is OutOfRa- **hypercast_version** — a zero-argument export returning theor << 16 | minor << 8 | patch. The cheapest possible proof thatthe library a host loaded is the one its binding was built again- RawNumFormat is 32 bytes. corpus/decimal.json is new; intined currency vectors. The fuzz target, the counting-allocatorproof and the fault-span sweep cover all of it.## Every binding- The 32-byte format crossing on every backend, native and wasm.- The decimal door mapped to the platform's exact type — decima decimal.Decimal (Python), Foundation.Decimal (Swift) — or anexact carrier where there is none: Go Decimal through the Exacimal with to_r, canonical to_s and a lazily required to_d,PHP HyperCast\Decimal with the magnitude as a numeric string.- The currency symbol filled in from the platform's own locale dnfo), NumFormat.from(Locale), NumFormat.from(locale:),from_localeconv, fromLocaleconv.- An availability probe that never throws, and the loaded core'silable/NativeVersion, Cast.isAvailable()/nativeVersion(),Available()/NativeVersion(), Cast.isAvailable/nativeVersi/native_version, hypercast.native_version(),Cast::isAvailable()/nativeVersion(). A consumer keeping a marobe instead of catching a load failure around its first realcast.## Binding ergonomics, ported system-wideEach began as a C# finding and was carried to every binding wheric home for it:- **One numeric door generic over the target** — C# Cast.Numeri numeric targets, Go Numeric[V Number] with the eleven as aconstraint, Swift Cast.numeric<T> closed over a NumericCastTano generics over primitives) nor the dynamic bindings, where theper-width doors are the idiom.- **Fault spans in the caller's own units** — the core reports Uring doors, Java String doors, Python str and Ruby textinputs now remap to char or code-point units on a non-ASCII fail text out of what was passed needs no mapping. Byte input andASCII text are untouched. Go, Swift and PHP strings are already- The culture bridge in the shape the platform already uses ider)andFrom(NumberFormatInfo) in C#,NumFormat::fromLocaleconv() in PHP.## Receipts- **HyperCast.Corpuson NuGet** — the conformance corpus as a ed in lockstep withHyperCast, packed, attested and pushed bythe same release run. NuGet only, on purpose: the corpus is a rems' receipts are their own suites replaying it.- **Benchmarks recorded as measured.** In the core, cast_decima cast_f64 on the same text (30.5 vs 28.1 ns) and 58.8 ns with adeclared symbol and grouping. In C#, Cast.Decimal is a wash ag9 vs 94.8 ns) and the currency path is a 1.6–1.7x loss againstNumberStyles.Currency on both the decimal and double doors. Thncy path is isolated in docs/roadmap.md as the nextoptimization; what the loss buys is the reason it was asked for.- Every suite green on a fresh build: Rust native and under was AOT publish with zero trim diagnostics; Java native and GraalWasmplus a GraalVM Native Image smoke; Go on cgo, purego and wasmtimdle and wasm; Python native and wasm; PHP.## Upgrade notesSource-compatible for every binding's consumers, with three thin- **The native library and the binding move together.** A 0.3.0 cast fails at load, which the new probe reports rather than thefirst cast.- ALL changed value (95, from 31): code that stored the nuet; code that named the constant gains currency, which with nosymbol declared changes nothing. SEPARATOR_DETECT stays exclud- Non-ASCII fault spans on the C#, Java, Python and Ruby text ot bytes. The byte doors are untouched.Two source-level changes for direct users of the crate and the J struct literal of the three old fields no longer compiles — useNumFormat::new(decimal_sep, group_sep, flags)or{ ..NumFormaormatis a four-component record withcurrencySymbol` last, andthe three-argument constructor remains.

The wasm backends are opt-in and change nothing until asked for: any package.

v0.2.0

Choose a tag to compare

@buvinghausen buvinghausen released this 02 Sep 22:40

The native call was never the bottleneck. Every binding already made exactly one call into the Rust core per cast — what cost real time was what each language wrapped around it: a copied input, heap scratch for a 16-byte out-value, an object built to be thrown away. This release strips that carrier back in every binding and measures each one before and after on one machine in one session. The core is untouched; no door changed its verdict on any input, and the shared corpus still replays byte-identical through all eight packages.

Alongside that: the seventh Ruby gem (aarch64-mingw-ucrt), per-platform Native AOT receipts, provenance instructions in every README, and the release pipeline brought in step with what HyperUuid 0.2.x learned.

The scoreboard, again

The RFC 3339 timestamp door against each platform's own parser, linux-arm64, crossing and transcode included in every HyperCast figure, with 0.1.0's number beside it. Full receipts and methodology in the root README and each binding's own.

Language 0.1.0 0.2.0 Platform parser, same run
Swift 281 ns, 3 mallocs 55 ns, 0 mallocs 817 ns Date.ISO8601FormatStyle 14.9x faster
Java 62.9 ns 52.5 ns (46.2 from byte[]) 596 ns Instant.parse 11.4x faster
C# 71 ns 76.9 ns from string, 51.2 ns from UTF-8 282 ns DateTimeOffset.TryParse 3.7x / 5.5x faster
Ruby (Magnus) 713 ns 771 ns 3.32 µs Time.iso8601 4.3x faster
PHP 487 ns 567 ns 1.57 µs DateTimeImmutable 2.8x faster
Go 174 ns, 2 allocs 112 ns, 0 allocs 67 ns time.Parse(RFC3339Nano) slower — still on purpose, now without an allocation

Where the carrier was the cost, the door moved. Where it wasn't — Ruby and PHP, whose numeric doors are what changed — the timestamp row is within noise of 0.1.0 and is printed as such.

Swift: zero mallocs on every door

Every String door copied the input into a fresh [UInt8], and every door then allocated three more heap arrays for the out-value, the fault span and the format — four mallocs before the native call. The input now crosses as a view of the string's own UTF-8 (withUTF8), the scratch is a tuple of fixed-width integers on the stack, and the library handle is a class reference rather than a 21-field struct copied out of a Result per call.

Door 0.1.0 0.2.0 mallocs
Cast.timestamp 281 ns 55 ns 3 → 0
Cast.uuid 222 ns 37 ns 3 → 0
Cast.f64 354 ns 45 ns 4 → 0
Cast.i32 349 ns 33 ns 4 → 0
Cast.dateTime (messy civil) 330 ns 129 ns 3 → 0

The Foundation controls in the same run are unchanged between the two tapes, which is what makes the comparison a receipt. Every door also gained an UnsafeRawBufferPointer overload — the primitive the String and [UInt8] forms now wrap.

Go: the verdict comes back by value

0.1.0's doors passed &out and &fault into the foreign call, and any Go pointer handed to cgo escapes to the heap. The README called that "a floor for this call shape" — it was a floor for that shape, not for the ABI. The C shims now declare the out-value, fault span and format on their own stack and return one struct, so no Go pointer crosses at all:

Door 0.1.0 0.2.0
Bool 111 ns, 1 alloc 78 ns, 0 allocs
I32 172 ns, 3 allocs 87 ns, 0 allocs
F64 218 ns, 3 allocs 106 ns, 0 allocs
Uuid 155 ns, 2 allocs 99 ns, 0 allocs
Timestamp 174 ns, 2 allocs 112 ns, 0 allocs
DateTime (1/7/2026 3:04 PM) 173 ns, 2 allocs 122 ns, 0 allocs — parity with time.Parse now

purego is unchanged within noise: its allocations are the trampoline's own argument boxing, not the out-params. Go still loses per-call to its stdlib on the plain doors, and still says so.

Java: the input no longer copies

Every downcall is linked Linker.Option.critical(true), so the caller's byte[] crosses as a pinned heap segment instead of being copied into a per-thread native staging buffer — that buffer and its arena are gone. Sound because every door is a short, non-blocking parse over those bytes that never calls back into Java, which is the profile the option exists for. reachability-metadata.json registers it, and the GraalVM Native Image smoke test passes on it, so a consumer's own native-image build inherits it with no configuration.

Every door gained a MemorySegment overload: slice one buffer holding many values — a mapped file, a direct buffer, one line of a CSV — and cast a value out of it with nothing copied. That is the shape round three's chunk layer will hand down. The UUID door reads its sixteen bytes as two big-endian longs instead of one at a time, which turned the one row that lost in 0.1.0 into a win:

Door 0.1.0 0.2.0 JDK
Cast.timestamp 62.9 ns 52.5 ns 596 ns Instant.parse
Cast.uuid 53.8 ns 38.3 ns 45.9 ns UUID.fromString
Cast.time 66.4 ns 45.1 ns 393 ns LocalTime.parse
Cast.dateTime (messy civil) 81.8 ns 64.9 ns 386 ns DateTimeFormatter

Full-length JMH, 2 forks × (5 warmup + 10 measurement), -prof gc.

Python, Ruby, PHP, C#

  • Python — cast_uuid builds uuid.UUID the way HyperUuid pinned: UUID.__new__ plus object.__setattr__ of the int and is_safe slots, skipping an __init__ whose validation the core had already done. 1.18 µs → 730 ns, ahead of uuid.UUID()'s 979 ns. The door used to sit at parity because both sides were bounded by __init__.
  • Ruby (Magnus) — the three reason Symbols and the option-symbol tables are cached, DETECT is identity-matched like INVARIANT, and any other format is resolved once per thread and memoized by identity, anchored in a thread-variable so the key can never be a recycled address. The numeric doors under a declared eurozone format or DETECT drop from ~990 to ~580 ns; every format other than INVARIANT used to pay three method dispatches and two String allocations per call to read its separators back out.
  • Ruby (Fiddle) — ~20% off the numeric doors (i32 3.37 → 2.62 µs) by not building per call what never changes: the interpolated :cast_* Symbol, the splat dispatcher, the 12-byte format copy into scratch. Still the zero-compile fallback, still on Fiddle's 1.6 µs floor.
  • PHP — the eight integer doors are flat (one literal FFI call each, no dynamic symbol lookup and string match per call), and Cast::uuidBytes returns the sixteen RFC-ordered octets for a BINARY(16) bind. Within noise on phpbench; recorded as structure, not speed.
  • C# — the string doors try the stack buffer first and rent from the pool only when the encoder says the text did not fit. New UTF-8 benchmark rows measure the ReadOnlySpan<byte> doors without the transcode every string row includes: Cast.Uuid 36.9 ns against Guid.TryParse's 51.2, which the string row had reported as a wash.

Ruby: the seventh gem

aarch64-mingw-ucrt joins the platform gems, with the Magnus extension for both Ruby ABIs, so Windows-on-ARM leaves the Fiddle fallback like every other mainstream platform. Both Windows extensions build the gnullvm targets. This one is CI's receipt so far — built and tested on the win-arm64 leg — and no Magnus-versus-Fiddle numbers have been taken on Windows-on-ARM hardware for this crate yet. gem install hypercast resolves the right one; anything outside the grid still gets the universal zero-compile gem.

Provenance, and the receipts behind it

Every README now has a Verifying provenance section: which artifact, which signer, and why --signer-repo SkunkWerkx/.github is needed on some and not others.

# A package — signed by release.yml, which lives in this repo.
gh attestation verify hypercast-0.2.0-aarch64-mingw-ucrt.gem --repo SkunkWerkx/HyperCast

# A native library, or a Magnus extension — signed by the shared forge workflow,
# so the signer's own repo has to be named too.
gh attestation verify libhypercast.so \
  --repo SkunkWerkx/HyperCast --signer-repo SkunkWerkx/.github

CI also publishes HyperCast.AotSmokeTest under Native AOT on all six RIDs, fails on any trim diagnostic, runs the binary, and uploads each leg's log as aot-report-{rid} — the AOT claim as a per-platform receipt rather than a README sentence. The crate is packaged and attested before its irreversible push, and publishes tokenless through Trusted Publishing.

Not in this release

A batch door. docs/roadmap.md places it in round three as new exports beside the scalar ABI; a binding-level loop over N crossings would be a fake win, and every change above is the per-call shape that layer will inherit. Two platform controls moved between toolchains and are reported as measured rather than carried over: Swift's DateFormatter read 810 ns on 0.1.0's tape and 28 µs on Swift 6.3.3, and the JDK's ISO_OFFSET_DATE_TIME control was unstable in the full-length run and is not quoted.

Install

Language
Rust cargo add hypercast crates.io
C# dotnet add package HyperCast nuget.org
Java io.github.skunkwerkx:hypercast:0.2.0 Maven Central
Python pip install hypercast PyPI
Ruby gem install hypercast RubyGems
PHP composer require skunkwerkx/hypercast Packagist
Swift .package(url: "https://github.com/SkunkWerkx/HyperCast", from: "0.2.0") g...
Read more

v0.1.0 — the first coordinated release

Choose a tag to compare

@buvinghausen buvinghausen released this 31 Aug 23:42

Every runtime already has TryParse. What it hands back is a bool and a shrug — no reason, no location, and none of the notations untrusted sources actually send. HyperCast returns a verdict instead: the value, or Empty / Malformed / OutOfRange plus the exact byte span that offended. One allocation-free engine written in Rust, called directly from C#, Java, Go, Swift, Ruby, PHP and Python — and as of this tag, every one of those eight is real: published, verified by an actual external install rather than a green CI run, and returning bit-for-bit identical verdicts from the same input on every platform.

This is the first coordinated release. Nothing here is a placeholder or a "coming soon" — see The doors for the full grammar each door accepts, and Receipts for exactly what has been proven on which platform, and how.

Install

Language
Rust cargo add hypercast crates.io
C# dotnet add package HyperCast nuget.org
Java io.github.skunkwerkx:hypercast:0.1.0 Maven Central
Python pip install hypercast PyPI
Ruby gem install hypercast RubyGems
PHP composer require skunkwerkx/hypercast Packagist
Swift .package(url: "https://github.com/SkunkWerkx/HyperCast", from: "0.1.0") git tag
Go go get github.com/SkunkWerkx/HyperCast/go@go/v0.1.0 git tag

Go's module path is versioned under its own go/vX.Y.Z tag (a Go modules requirement for a subdirectory module) rather than the bare tag every other binding uses — that's already pushed alongside this release, not a separate step.

The doors

Twenty cast_* exports over a plain C ABI: boolean (twenty lexemes — true/false, yes/no, on/off, enabled/disabled, in/out, …), the full integer family (i8–i64, u8–u64), reals (f32/f64), UUID (all five .NET Guid text forms plus urn:uuid: prefixes), RFC 3339 timestamp, Unix epoch at a declared precision, date and time, local date-time (the AM/PM world, with no zone read and none invented), declared-order separated dates, Excel serial dates, and duration (ISO 8601, invariant colon form, protobuf JSON seconds).

Culture never lives in the core. Numeric doors take a caller-declared NumFormat — separators plus individually declarable leniences — and separated dates take a caller-declared DateOrder, because 1/7/2026 is January 7th or July 1st only if you say which. NumFormat.DETECT resolves ./, roles structurally per input, and reports the genuinely ambiguous (12.185, 1,000) as Malformed at the separator rather than guessing.

The scoreboard

Real numbers, not adjusted for story. The RFC 3339 timestamp door against each platform's own parser, linux-arm64, with the FFI crossing and transcode included in every HyperCast figure. Full receipts and methodology in the root README and each binding's own.

Language HyperCast Platform parser
Java 62.9 ns 561 ns Instant.parse 8.9x faster
C# 71 ns 286 ns DateTimeOffset.TryParse 4.0x faster
Ruby 713 ns 2.88 µs Time.iso8601 4.0x faster
Swift 278 ns 837 ns Date.ISO8601FormatStyle 3.0x faster
PHP 487 ns 1.3 µs DateTimeImmutable 2.7x faster
Python 201 ns 163 ns fromisoformat (C-accelerated) near-parity
Go 175 ns 67 ns time.Parse(RFC3339Nano) slower — on purpose, and printed

The gap is widest on the messy-feed doors, where most platforms have no parser for the input at all and strptime is the only thing that accepts it: 1/7/2026 3:04 PM under a declared order is 12.7x faster than datetime.strptime in Python, 4.1x than a Java DateTimeFormatter, 3.6x than DateTime.TryParse on .NET.

Go stays in the roster losing per call on purpose: its stdlib RFC 3339 path is genuinely excellent and there is no FFI boundary to beat, and the honest result is what makes the other wins credible rather than a foregone conclusion. Individual losses elsewhere are reported the same way — Boolean.parseBoolean and UUID.fromString on the JVM, int.TryParse with AllowThousands on .NET, Ruby's civil-date door.

What's proven, and how

  • The corpus is the contract. 380 vectors across twelve files replay through the Rust core's suite and every binding's, so all eight agree byte for byte on values and fault spans. Ruby replays it twice, once per backend.
  • Allocation-free is asserted by a counting allocator, not a doc comment — 1000 calls to every door, success and failure paths, zero allocations demanded. A fault is a span into your buffer; nothing is captured or formatted on the error path.
  • The Rust core is #![no_std] and links no allocator. A bare-metal consumer supplies a #[panic_handler] and stops there. Both halves are CI-guarded, not asserted.
  • Fuzzed, and it found real bugs. A cargo-fuzz target drives all 20 doors under six format profiles and every declared order/precision/epoch. It caught two real classes within a minute — truncation faults pointing one byte past the input, and spans overrunning on text ending mid-UTF-8-character — both fixed structurally and pinned as ordinary tests. The following 550M-execution session found nothing.
  • AOT on both managed platforms, proven against the published artifacts rather than the working tree: C# under PublishAot into a real native binary, Java through a real GraalVM Native Image build that inherits the FFM signatures and resources glob from the shipped reachability-metadata.json with zero configuration.
  • Exhaustiveness is a compile error where the language can enforce it — a native [Union] with CS8509-as-error in C#, a sealed interface in Java, a compiler-mandatory-exhaustive enum in Swift.

What's known and out of scope, on purpose

  • The tabular layer is not in this release. CSV/TSV/XLSX ingestion — and the JSON layer behind it — is the endgame, and it's the reason the doors are shaped the way they are: zero allocation per cast and verdict-as-span exist so a batch layer can cross the FFI boundary once per chunk instead of once per cell. A million-row, 20-column file is 20M scalar casts. See docs/roadmap.md.
  • WebAssembly is proven for the Rust core — the full suite (66 tests) passes under wasmtime on wasm32-wasip1. The Blazor browser-wasm story is proven as far as packaging and linking go; it has not yet been run in a real browser session, and isn't claimed as such until it has. Python's Pyodide path left deliberately with the retired ctypes backend.
  • Per-language floors are each driven by a feature the verdict type actually needs: .NET 11, JDK 22, Python 3.10, Ruby 3.2, PHP 8.1, Swift tools 5.9 / macOS 13, Go 1.26.

Verification

Every binding in this release was pulled from its real, live registry — or resolved via its real git tag — into a fresh scratch project and run, because "the publish succeeded" and "a consumer can use it" are different claims. Three of the four bugs this project has shipped were found exactly in that gap, and none of them could have failed a build in this repository.

Thanks

If you're reading this because you care about honest performance work rather than marketing claims — that's exactly the audience this project was built for.