Repository navigation
Releases: SkunkWerkx/HyperCast
Release list
v1.0.0
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()declaredthrowsand never threw: bad data is theVerdict'sFault,
and the core is linked in, so there is nothing to fail to load. Call sites need notry;
one left in place compiles with a warning. (Swift) - C# —
HyperCast.targetsno longer lists anEmccExportedFunctionper entry point. The
NativeFileReferenceis 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
andLibraryImport("*")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.
PushingvX.Y.Zpacks 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.ymlwithprepared_run_idpublishes 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/v1answers 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 ofrelease.yml,
prepare-release.yml,stage-native-binaries.ymland every static check inci.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.jsonand.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 undergo/staticlib,swift/HyperCastCore.artifactbundleand
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'srust/differs from the commit those binaries were
built from: Go and PHP before any test, Swift as a failingCoreFreshnessTests.
.github/scripts/local-core.shalso 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) raisedValueError
there. The docs in every binding now state the rule the core enforces, where they said
"one or two characters". (Python) -
Ruby —
NumFormat.from_i18nfills a missing mark instead of raising.
from_i18n({separator: ","})raisedArgumentError, 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'sfrom_localeconvand PHP's
fromLocaleconvalready did. (Ruby) -
Rust — safe scalar representation boundaries.
Symbol<N>rejects strings longer
than its one-byte length can carry, including whenNexceeds 255, preserving
UTF-8 boundaries.Decimaldisplay 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 whileglobal.jsonpins 11.0.100-rc.2. The abort on the
trimmable-staticregistrar still reproduces on SDK 11.0.100-rc.2.26504.105, so the
smoke test keepsmanaged-staticthere 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-wasip1module, andCast.backend(), thehypercast.backendproperty 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, thewasm32-wasip1export flags inrust/.cargo/config.tomland
the-Pwasmruns 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,LoadErrorandNativeVersionstay. (Go) - Swift —
NativeLibraryError. The deprecated error type had no cases and was never
thrown, since the core is linked in on every platform.Cast.isAvailablestays, 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'sHyperCast::BACKENDstays, 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/v1The 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 07c0f9a140eed2899c64b5783a7c0eeb6d04794binventory.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
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.
NumFormatgains, beside the separators, flags and currency symbol it had:minus_signandplus_sign(U+2212 for Estonian and Basque; an RTL mark and a hyphen).currency_decimal_sepandcurrency_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-DEwrites1.234,5and€1,234.50).percent(٪,%) anddigits, 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 ofALL; every bridge sets it from the culture's own negative pattern.
corpus/integer.json,real.jsonanddecimal.jsonpin 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'scast_datetimetakes aRawDayPeriodspointer after the order, null for English only.corpus/datetime.jsonpins 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>separatorscast_datetime_with(text, order, &DayPeriods)— C# init-only MinusSign,PlusSign,CurrencyDecimalSeparator,CurrencyGroupSeparator,PercentSymbol,DigitZero(Rune?);NumStyles.TrailingSignDateFormat(order, am, pm);Cast.DateTimeoverloads forDateFormatandIFormatProvider?NumFormat.FromandDateFormat.Fromread every field fromNumberFormatInfoandDateTimeFormatInfo; neither throws on any runtime's ICU or NLS dataJava with*for each field;STYLE_TRAILING_SIGNDateTimeFormat(order, am, pm);Cast.dateTimeoverloads forDateTimeFormatandLocaleNumFormat.from(Locale)and newfrom(DecimalFormat); `DateSwift minusSign,plusSign,currencyDecimalSeparator,currencyGroupSeparator,percentSymbol,digitZero;.trailingSignDateFormat(order:amSymbol:pmSymbol:);dateTime(_:format:)anddateTime(_:locale:)NumFormat.from(locale:)and newfrom(formatter:);DateFormat.from(locale:)PHP Named constructor arguments; NumFormat::TRAILING_SIGNnew DayPeriods($am, $pm)as the third argument ofCast::datetimefromLocaleconvandfromNumberFormatterread the monetary separators, signs, percent, zero digit and negative suffixRuby Keywords on the Data;HyperCast::TRAILING_SIGNHyperCast.datetime(text, order, am:, pm:)NumFormat.from_i18nreadsnegative_formatand takescurrency_format:Python Keyword-only constructor arguments; NumFormat.TRAILING_SIGNcast_datetime(text, order, am=, pm=)NumFormat.from_localeconvreads the monetary separators and signsThe boolean door stays English, by decision: no platform's culture data carries a boolean lexicon to bridge. A feed that says
ja/ouimaps its words before the door. -
The format crosses the C ABI as a new, size-tagged layout.
RawNumFormatis a size word followed byNumFormatitself, now#[repr(C)]: 92 bytes, read by the core in place, so a cast pays for a check and no copy.RawDayPeriodsis 40 bytes.rust/src/abi.rshas the offset table, and every binding restates it and pins it in its interop tests. Measured through the C ABI, a groupedcast_i32under 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_sepandgroup_separeSymbol<8>where they werechar;NumFormat::newstill takeschars. -
Python: free-threaded CPython, with the GIL off. Each platform gets three wheels where it had one:
cp311-abi3for CPython 3.11+ with the GIL as before,cp314-cp314tfor free-threaded 3.14, andcp315-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 afterimport 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'spython-valuesfeature is now built on PyO3 0.30. -
C#: Blazor WebAssembly. A publish without the
wasm-toolsworkload used to skip linking the native core and ship a runtime with nocast_*symbols, with nothing louder than the SDK's own warning. The package's targets now fail the build withHYPERCAST002; an app that gates onCast.IsAvailableand keeps a managed fallback setsHyperCastRequireWasmWorkloadtofalse. The browser-wasm archive moves toruntimes/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.ymlnow 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 theTryGetValuechain. - **P
ffi.enable=preloadthe first numeric door failed with "FFI API is restricted". It now preloads both directories.
Upgrading
- Rust:
version = "0.9"inCargo.toml. A crate usingpython-valuesmoves 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-callfromNumberFormattermemo now reads twelve attributes and costs about four times a hoistedNumFormat, so hoist it. - Swift:
from(locale:)derives afresh for.currentand.autoupdatingCurrentso a user's overrides are honoured; a hot loop passes an explicit locale or a heldNumFormat. - 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/.githubThe 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
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: decimal65,U+0041,0x41,&H41, or an HTML entityA/A. A surrogate or anything pastU+10FFFFisOutOfRange. It is the twenty-sixth C ABI export,cast_char, andcorpus/char.jsonis replayed by all eight suites.Language Door Returns Rust cast_charcharC# Cast.CharcharJava Cast.characterCharacterGo CharruneSwift Cast.charUnicode.ScalarPHP, Ruby, Python Cast::char,HyperCast.char,cast_chara one-character string C# and Java return a scalar past
U+FFFFasOutOfRange, since theircharis 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
Troutes through HyperCast in one call.- C#:
Cast.Scalar<T>overbool, the numerics,Guid,DateOnly,TimeOnly,DateTimeOffset,TimeSpan,charandDateTime.Scalar<DateTime>reads RFC 3339 asUtcDateTime. - Go:
Scalar[V ScalarTarget, T Text]. A type outside the union is a compile error. - Swift:
Cast.scalar<T: ScalarCastTarget>(_:format:).
- C#:
-
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
NumFormatfirst. Each maps to the core's format with every lenience on, and aNumFormatis still how to declare anything stricter.- C#: an
IFormatProvider?overload of every numeric door,Numeric<T>andScalar<T>.nullis the current culture, as inTryParse. - Java: a
Localeoverload 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,numericandscalar. - PHP: every numeric door accepts
NumFormat|\NumberFormatter, plusNumFormat::fromNumberFormatter. intl stays optional. - Ruby: Rails'
separator:,delimiter:andunit:keywords on every numeric door, plusNumFormat.from_i18nfor anI18n.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. - C#: an
-
Android, in C#, Go and Swift, on arm64 and x86_64.
Language How C# The package carries android-arm64andandroid-x64, so a .NET MAUI app has the core on every platform MAUI targets with nothing but the package reference. CoreCLR loadslibhypercast.sofrom 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=androidlinksgo/staticlib/android_{arm64,amd64}, with the NDK's clang asCC.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,Decimaland 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 reportingundefined: symChar.
Upgrade notes
Source-compatible for every consumer except one narrow case.
- Java: a call that passed a literal
nullas the format, such asCast.i32(text, null), no longer compiles, because the newLocaleoverload makes it ambiguous. It already threw at run time, so no working code depends on it. - Rust:
version = "0.8"inCargo.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/.githubThe 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
Troutes through HyperCast in one call.- C#:
Cast.Scalar<T>overbool, the numerics,Guid,DateOnly,TimeOnly,DateTimeOffset,TimeSpan,charandDateTime.Scalar<DateTime>reads RFC 3339 asUtcDateTime. - Go:
Scalar[V ScalarTarget, T Text]. A type outside the union is a compile error. - Swift:
Cast.scalar<T: ScalarCastTarget>(_:format:).
- C#:
-
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
NumFormatfirst. Each maps to the core's format with every lenience on, and aNumFormatis still how to declare anything stricter.- C#: an
IFormatProvider?overload of every numeric door,Numeric<T>andScalar<T>.nullis the current culture, as inTryParse. - Java: a
Localeoverload 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,numericandscalar. - PHP: every numeric door accepts
NumFormat|\NumberFormatter, plusNumFormat::fromNumberFormatter. intl stays optional. - Ruby: Rails'
separator:,delimiter:andunit:keywords on every numeric door, plusNumFormat.from_i18nfor anI18n.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. - C#: an
-
Android, in C#, Go and Swift, on arm64 and x86_64.
Language How C# The package carries android-arm64andandroid-x64, so a .NET MAUI app has the core on every platform MAUI targets with nothing but the package reference. CoreCLR loadslibhypercast.sofrom 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=androidlinksgo/staticlib/android_{arm64,amd64}, with the NDK's clang asCC.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,Decimaland 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 reportingundefined: symChar.
Upgrade notes
Source-compatible for every consumer except one narrow case.
- Java: a call that passed a literal
nullas the format, such asCast.i32(text, null), no longer compiles, because the newLocaleoverload 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,Decimaland the two built-in formats) in a Rust static that a compacting GC co...
v0.7.0 - interop, numbers already held, faster, and iOS
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 coversios-arm64,iossimulator-arm64,maccatalyst-arm64andmaccatalyst-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, whichgomobilesets).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*FromCodefunctions - Python:
NumFormat.packed - Rust:
from_codeon the enums, a publicRawNumFormat, and apython-valuesfeature - Rust's C symbols are now an
exportsfeature, so a crate that exports its own C ABI can turn them off.
- C#:
-
Numbers already held. A workbook stores a numeric cell as a double, not text. Four new doors read that
f64directly as:- an exact decimal
- an Excel serial
- a time of day
- a span
That takes the C ABI to 25
cast_*exports plushypercast_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.5is 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_timestamp26.6 → 14.5 ns,cast_datetime29.3 → 13.9 ns,cast_duration35.5 → 23.6 ns. cast_f64andcast_f32are 22-36% faster.cast_uuidis about 20% faster.
Bindings that cross cheaply keep most of it. In Swift, duration went from 45 to 33 ns. C#'s
Cast.Durationwent from 49.1 to 31.6 ns. The shared library grows about 10 KB for the inlining. - The temporal doors are about twice as fast:
-
Every door is now proven unable to panic.
cast_f32andcast_f64were the last two outside the link-time proof. Their float conversion is now HyperCast's own code and gives the same bits ascore'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.0000000001wasMalformed. 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 = falsebuild no longer carries the C ABI symbols. Nameexportsbesidestdif you relied on them. Default builds are unchanged. - PHP:
HyperCast\NativePlatform(always@internal) is nowHyperCast\Interop\NativePlatform, and its methods take the library's base name first. - Java:
CastFailure.fromCodeis public and returnsOptional<CastFailure>instead of throwing. It was package-private before. - Go:
GOOS=androidno 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/.githubThe 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
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-wasmgem. ruby.wasm links extensions into the interpreter whenrbwasm buildmakes it. So in the Gemfile you hand torbwasm build, listhypercast-wasminstead ofhypercast: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, soHyperCast::BACKENDis:nativeand 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-versionpicks. - Shares an interpreter with HyperUuid:
hyperuuid-wasmlinks 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!
- Same extension: you get the same Magnus extension as the platform gems, prebuilt for
Fixes
- Ruby: the Magnus extension compiles on 32-bit targets. It held raw
VALUEs asu64, which only type-checks whereVALUEis 64 bits; the firstwasm32-wasip1build caught it. Nothing changes on the platform gems' targets. - Rust: the
no-panicproof from a consumer's crate now passes withlto = truealone, as the README says.cast_uuidused to keep a bounds check that depended on the optimizer; it now reads every byte withget. 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
HyperCasttypes. The targets file now ships underbuild/net11.0/andbuildTransitive/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.Errorreturns<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/.githubThe 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
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 beforeRust wasm32-unknown-unknown, no extra setupPython (new) Pyodide: await micropip.install("hypercast")finds a newpyemscriptenwheel on PyPI (~150 KB). The whole test suite runs inside Pyodide in CI.Go (new) TinyGo 0.42+ with -target=wasm, the core linked inSwift (new) the wasm32-wasip1build, 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/.
- Removed: purego, the loading cgo backend, the wasmtime backend, and the nine embedded libraries under
-
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_f64is proved at link time unable to panic. The two float doors' remaining paths are insidecore'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.
- Panics: every door except
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-baseon Alpine) - macOS: the Xcode command-line tools
- Windows: MinGW-w64 gcc (llvm-mingw on arm64)
- Linux: gcc or clang (
- Unsupported builds fail at compile time, with an error that names the fix. That covers
CGO_ENABLED=0, stock Go'sGOOS=js/wasip1, and any other platform. Use TinyGo for WebAssembly. - Drop the old build tags
-tags hypercast_dynamicand-tags hypercast_wasm. - API unchanged:
Available()is alwaystrue, andErrNativeUnavailableis deprecated.
Every package: impossible dates and times are OutOfRange, not Malformed.
Malformednow 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:60leap second, an offset past23:59, the colon duration25:00:00, and Excel's phantom 1900 serial60. - 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:00xisMalformedat thex. - 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/.resourcesbeside your executable. NativeLibraryErrorhas no cases now, so code matching.openFailed/.symbolNotFoundno 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-linuxor 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_WASMis 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 = falsebuilds on every target again. Through 0.4.0 it failed with "#[panic_handler]function required" everywhere but bare metal. - Rust:
cast_uuidnow passes theno-panicproof from a consumer's crate too. Running it from your own crate needslto = truein your release profile. - C#: a Blazor WebAssembly app no longer gets the 3 MB
libhypercast.ainbin/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-rsextension runs as a WordPress Playground side module (258 KB, 85 KB gzipped), andphp/README.mdhas 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/.githubThe 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
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.67and($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 groupedi6441–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-x64andlinux-musl-arm64are built, attested and shipped in every package (asmusllinux_1_2wheels 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 orscratchimage. - 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-imagebuild needs no configuration. - Python doors that return an object cost about half.
cast_uuid557 → 262 ns,cast_timestamp369 → 252 ns,cast_datetime339 → 230 ns,cast_duration581 → 262 ns. Wheels are unchanged (still oneabi3wheel per platform), and the package is now typed. - Ruby doors on the Magnus backend cost a quarter of what they did.
bool462 → 112 ns,i32468 → 133 ns,uuid629 → 223 ns,timestamp794 → 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-mbstringis no longer needed;ext-ffiis the only required extension. - PHP, Python:
NumFormat::fromLocaleconv/from_localeconvnever 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
uuiddoor returns a US-ASCII String on every backend. - Go:
LoadError()says why the core did not load, and the doors panic withErrNativeUnavailableso arecovercanerrors.Isit.-tags hypercast_dynamickeeps the previous loading cgo backend. - Swift:
NativeLibraryErroris public andLocalizedError. - C#: set
HyperCastStaticLink=falseto 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 precompiledx86_64-darwingem; Ruby there runs on the Fiddle backend.
Fixes
- C#: a
NumFormatwith no currency symbol set could throwNullReferenceException;DateOrders.From(null)now throwsArgumentNullException. - 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)raisedTypeErroron the Magnus backend though nil is the documented default; on Alpine, 0.3.0 loaded the glibc library andavailable?was false. - Swift: a missing resource bundle crashed the process,
Cast.isAvailableincluded. The probe is nowfalseand the doors throw. - Python:
bytesinput cost about a microsecond more thanstron 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/.githubv0.3.0 - what the first consumer asked for
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
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_uuidbuildsuuid.UUIDthe way HyperUuid pinned:UUID.__new__plusobject.__setattr__of theintandis_safeslots, skipping an__init__whose validation the core had already done. 1.18 µs → 730 ns, ahead ofuuid.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,
DETECTis identity-matched likeINVARIANT, 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 orDETECTdrop from ~990 to ~580 ns; every format other thanINVARIANTused to pay three method dispatches and twoStringallocations per call to read its separators back out. - Ruby (Fiddle) — ~20% off the numeric doors (
i323.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::uuidBytesreturns the sixteen RFC-ordered octets for aBINARY(16)bind. Within noise on phpbench; recorded as structure, not speed. - C# — the
stringdoors 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 theReadOnlySpan<byte>doors without the transcode everystringrow includes:Cast.Uuid36.9 ns againstGuid.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/.githubCI 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... |
v0.1.0 — the first coordinated release
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-fuzztarget 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
PublishAotinto a real native binary, Java through a real GraalVM Native Image build that inherits the FFM signatures and resources glob from the shippedreachability-metadata.jsonwith zero configuration. - Exhaustiveness is a compile error where the language can enforce it — a native
[Union]with CS8509-as-error in C#, asealed interfacein Java, a compiler-mandatory-exhaustiveenumin 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
wasmtimeonwasm32-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.