Skip to content

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
· 75 commits to master since this release
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.