hypercast-wasm 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10c626d762b241c551b0120df1d77ec78d4831a9328c670d2a7bc50c4f3cdf42
4
- data.tar.gz: 3fbb217bfe1717648b5a579e034894c2eb55fc797f232b19d2f41aaf950299c2
3
+ metadata.gz: e8fba4b024f4c3f507d9c5d31187c8434d704e8ce0b613a5dd0d8de1100716e8
4
+ data.tar.gz: 9156a91e12b17035fa8e7cca34d79d0990e20388b6037386a1555e2a7f9526a9
5
5
  SHA512:
6
- metadata.gz: 952da6145e72b6d45cda1cb1e56351941cd19f20008a5963ab0452c8c7eec2e6e4204de281d704f2dcf8967d2a8c4a12f0c69984528cd8fdd12e26daded490a2
7
- data.tar.gz: e924d2a7f955fd4f748bea86a9e11b8a05343608e4eda4358c9a324aa225088b045a6b569d318fd2b4acc443c847c5809d2ddd91fdbd5fe0aeae19bfcdd56d23
6
+ metadata.gz: c18b51f977271062987a6c62a34154786b37fdee697661c921846c7034d07178c49b731cce9074aff07864d63a9027f77e11d4c88a9cf101cdb514aa9419aa05
7
+ data.tar.gz: 6f6ab24b0d6331b2d8020c93ebfb831ebc84df0bb796b6add5a6b733c938bf83d3c8525f307f5141273a81c86e448f46981bb0b27f0496936871c06618c02f8f
data/README.md CHANGED
@@ -146,8 +146,8 @@ HyperCast::NumFormat.new(decimal_sep: ".", group_sep: ",", flags: HyperCast::ALL
146
146
 
147
147
  The flags are `GROUPING`, `PARENTHESES`, `EXPONENT`, `RADIX_PREFIXES`, `PERCENT` and
148
148
  `CURRENCY` (`ALL_STYLES` is all six), plus `TRAILING_SIGN` — deliberately not in
149
- `ALL_STYLES`; see below — and the `SEPARATOR_DETECT` policy. A separator is one or two
150
- characters (`", "`, comma then space, is a separator), up to 8 UTF-8 bytes.
149
+ `ALL_STYLES`; see below — and the `SEPARATOR_DETECT` policy. A separator is 1 to 8 UTF-8
150
+ bytes (`", "`, comma then space, is a separator).
151
151
 
152
152
  Everything past the separators and the flags is what a culture table would fill in, and
153
153
  every such field is declared, never looked up — `from_i18n` fills what Rails' locale data
@@ -250,12 +250,13 @@ any `NumFormat`, and the flags and every other field are left alone, so a `unit:
250
250
  format without `CURRENCY` is declared but not honored, as it would be in the constructor.
251
251
  `from_i18n` reads Symbol or String keys and ignores every other key, so
252
252
  `number.currency.format` passed alone works as well. Missing keys keep the invariant's
253
- fields. An empty `delimiter` (a locale that writes no grouping) turns `GROUPING` off. A
253
+ fields, except that a missing mark never repeats the one the Hash declares:
254
+ `{separator: ","}` groups with `"."`, as Python's and PHP's locale bridges do. An empty `delimiter` (a locale that writes no grouping) turns `GROUPING` off. A
254
255
  `negative_format` that writes the sign after the number (`"%n-"`, `"%n %u-"`) turns
255
256
  `TRAILING_SIGN` on; `"-%n"`, the Rails default, leaves it off. `currency_format:` takes
256
257
  `number.currency.format` beside `number.format`: its `separator` and `delimiter` become
257
258
  `currency_decimal_sep` and `currency_group_sep` (a key it lacks declares nothing, so the
258
- number separator stands in), its `unit` the currency symbol, and its `negative_format`
259
+ number separator stands in, unless that would repeat the currency mark it does declare), its `unit` the currency symbol, and its `negative_format`
259
260
  counts the same way. `flags:` defaults to `ALL_STYLES`.
260
261
 
261
262
  That is everything the Hash offers. Rails carries no minus or plus sign, no percent symbol
@@ -263,8 +264,7 @@ and no digits, so `from_i18n` leaves those undeclared — the core reads ASCII `
263
264
  and `0`–`9` — and a locale that writes them differently declares them on the result:
264
265
  `from_i18n(I18n.t("number.format")).with(minus_sign: "−")` for Estonian.
265
266
 
266
- A call without the keywords is exactly the call it was, with no extra allocation on either
267
- backend. A call with them builds a format each time (a few microseconds), so in a loop,
267
+ A call without the keywords allocates nothing extra on either backend. A call with them builds a format each time (a few microseconds), so in a loop,
268
268
  build it once with `override` or `from_i18n` and pass that instead.
269
269
 
270
270
  ### `datetime`: a culture's AM/PM
@@ -274,8 +274,8 @@ the clock (`3:04 PM`) or ahead of it (`PM 3:04`). `am:` and `pm:` declare a cult
274
274
  words beside the English ones — German's `vorm.`/`nachm.`, Korean's `오전`/`오후`
275
275
  (written ahead of the clock), Spanish's `a. m.`/`p. m.` (a space inside is fine) — matched
276
276
  exactly, with or without a space between word and clock. English is still read whatever is
277
- declared; `nil`, the default, declares nothing, so a call without the keywords is exactly
278
- the call it was. Like a `NumFormat`'s symbols the words are declared, never looked up:
277
+ declared; `nil`, the default, declares nothing, so a call without the keywords reads the
278
+ English pair only. Like a `NumFormat`'s symbols the words are declared, never looked up:
279
279
  Ruby's stdlib has no day-period data, and `I18n.t("time.am")`/`("time.pm")` are where a
280
280
  Rails app finds them.
281
281
 
@@ -349,16 +349,14 @@ HyperCast::Interop.version(0x000700) # => "0.7.0"
349
349
  2.85 µs: a little behind `Time.iso8601`, sitting on Fiddle's per-call marshalling floor.
350
350
 
351
351
  Separator detection is nearly free here: `1.234.567,89` under `NumFormat::DETECT` runs
352
- at 154 ns against 145 ns for the same text under a declared eurozone format. Both cost
353
- nearly twice that in 0.1.0, for a reason that had nothing to do with parsing: every format other than `INVARIANT` paid three method dispatches and two
354
- `String` allocations per call to read its separators back out of the `Data`. `DETECT`
355
- is now identity-matched like `INVARIANT`, and any other format is resolved once per
356
- thread and memoized by identity — anchored in a thread-variable so the memo's key can
357
- never be a recycled address — which turned the per-call cost into one pointer compare.
352
+ at 154 ns against 145 ns for the same text under a declared eurozone format. Reading a
353
+ format costs one pointer compare: `DETECT` is identity-matched like `INVARIANT`, and any
354
+ other format is resolved once per thread and memoized by identity — anchored in a
355
+ thread-variable so the memo's key can never be a recycled address.
358
356
 
359
357
  A declared currency symbol costs text that does not carry one about 6 ns on the Magnus
360
- backend: in one run, `1,234,567` took 151 ns under `INVARIANT` and 157 ns under a format
361
- declaring `$`. A fault costs more than a success: the same run returned the `Fault` for
358
+ backend: in a run for 0.9.0, separate from the [Benchmarks](#benchmarks) table, `1,234,567`
359
+ took 151 ns under `INVARIANT` and 157 ns under a format declaring `$`. A fault costs more than a success: the same run returned the `Fault` for
362
360
  `12345x7` in 222 ns, against 101 ns for `Integer("12345x7", exception: false)` returning
363
361
  `nil`. The door builds a reason and a span where `Integer()` hands back nothing.
364
362
  The three reason Symbols and the option Symbols (`:seconds`, `:month_day_year`, …) are
@@ -366,8 +364,9 @@ HyperCast::Interop.version(0x000700) # => "0.7.0"
366
364
  `Symbol#name` materialization.
367
365
 
368
366
  **The honest trade-off:** the civil doors do not beat `strptime`. The date-time door is
369
- level with `DateTime.strptime` (904 ns against 901 ns) and the date door a little behind
370
- `Date.strptime` (620 ns against 513 ns). The parse isn't the cost; the carrier is. Building
367
+ level with `DateTime.strptime` (877 ns against 851 ns, the session of the
368
+ [table below](#benchmarks)) and the date door a little behind `Date.strptime` (620 ns against
369
+ 513 ns, measured for 0.4.0 and not re-timed since). The parse isn't the cost; the carrier is. Building
371
370
  a stdlib `DateTime` with an exact `Rational` second costs more than the whole native call,
372
371
  where the timestamp door's `Time` is built by a single cheap `rb_time_nano_new`. If you want
373
372
  Ruby's fastest civil parse and don't need the verdict or the declared order, `strptime` is
@@ -377,9 +376,8 @@ which is an artifact of the type, not a zone the parse assigned.
377
376
  On the Fiddle fallback the doors are parity-at-best — Fiddle's
378
377
  per-call floor is the mechanism's price, kept because it's the universal zero-compile
379
378
  path. Its doors build nothing per call that does not change between calls: each format
380
- owns one native pointer, memoized by identity and passed straight through. (Benchmark forensics worth knowing: the doors read 4.3 µs until
381
- per-call `Fiddle::Pointer.malloc` finalizers were hoisted to thread-local scratch —
382
- receipts include their own archaeology.)
379
+ owns one native pointer, memoized by identity and passed straight through, and its scratch
380
+ is thread-local rather than a `Fiddle::Pointer.malloc` per call.
383
381
 
384
382
  ## Benchmarks
385
383
 
@@ -433,33 +431,41 @@ buffers; the core itself keeps no state between calls. `spec/cast_spec.rb`'s
433
431
  concurrent-callers example runs under both.
434
432
 
435
433
  **Ractors.** Main Ractor only, on every backend: called from another Ractor the doors raise
436
- `Ractor::UnsafeError`.
434
+ (`Ractor::UnsafeError` from the Magnus extension, `Ractor::IsolationError` on Fiddle).
437
435
 
438
436
  ## Verifying provenance
439
437
 
440
- Every gem RubyGems.org serves — the universal fallback, each of the seven precompiled
441
- platform gems and `hypercast-wasm` — carries its own GitHub build-provenance attestation, signed directly by
442
- this repo's own `release.yml` (the `rubygems-publish` job attests `ruby/pkg/*.gem` right
443
- before the push), so plain `--repo` verifies any of them:
438
+ Every gem RubyGems.org serves — the universal fallback, each of the seven precompiled platform
439
+ gems and `hypercast-wasm` — verifies from the release tag alone: the release attestation,
440
+ signed by this repository's `release.yml` at the tag, names every one of them, with the
441
+ release inventory as its predicate.
444
442
 
445
443
  ```sh
446
- gem fetch hypercast -v X.Y.Z --platform <platform> # or omit --platform for the universal gem
447
- gh attestation verify hypercast-X.Y.Z-<platform>.gem --repo SkunkWerkx/HyperCast
444
+ gem fetch hypercast -v <version> --platform <platform> # or omit --platform for the universal gem
445
+ gh attestation verify hypercast-<version>-<platform>.gem --repo SkunkWerkx/HyperCast \
446
+ --source-ref refs/tags/v<version> --predicate-type https://github.com/SkunkWerkx/.github/predicates/release/v1
448
447
  ```
449
448
 
450
- That's the release's second layer of checking, not the only one: before any gem gets built,
451
- the same job verifies every native binary it packs — the Fiddle libraries, the Magnus
452
- extensions (one per Ruby ABI per platform), the `wasm32-wasip1` archives — against *their own*
453
- attestations — those are signed from `SkunkWerkx/.github` by `hyper-build-native.yml` and
454
- `hyper-build-wasm.yml`, so
455
- that check needs `--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an
456
- unverified one.
457
- RubyGems.org has no unpublish and no duplicate-version overwrite, so this all happens while a
458
- bad artifact is still reversible. The release run's job summary then re-fetches every gem
459
- from the CDN and records attested-vs-served digests, turning "rubygems.org stores an upload
460
- verbatim" into a per-release measurement rather than an assumption — see
461
- [csharp/README.md's provenance section](../csharp/README.md#native-binary-provenance) for
462
- more on why `--signer-repo` is needed for some artifacts here and not others.
449
+ Each gem's build provenance is this repository's too: the `ruby-pack` job attests
450
+ `ruby/pkg/*.gem` as soon as they are built and `rubygems-publish` pushes those preserved
451
+ bytes, so plain `--repo` verifies it:
452
+
453
+ ```sh
454
+ gh attestation verify hypercast-<version>-<platform>.gem --repo SkunkWerkx/HyperCast
455
+ ```
456
+
457
+ That is the release's second layer of checking, not the only one: before any gem is built, the
458
+ same job verifies every native binary it packs — the Fiddle libraries, the Magnus extensions
459
+ (one per Ruby ABI per platform), the `wasm32-wasip1` archives — against *their own* build
460
+ provenance, signed from `SkunkWerkx/.github` by `hyper-build-native.yml` and
461
+ `hyper-build-wasm.yml` (so that check needs `--signer-repo SkunkWerkx/.github` added), and
462
+ refuses to proceed on an unverified one. RubyGems.org has no unpublish and no
463
+ duplicate-version overwrite, so all of this happens while a bad artifact is still reversible,
464
+ and the push itself is a separate dispatch from the preserved bytes. The release run then
465
+ re-fetches every gem from rubygems.org and fails unless each matches the attested bytes
466
+ exactly, turning "rubygems.org stores an upload verbatim" into a per-release check rather than
467
+ an assumption. The root README's [Provenance](../README.md#provenance) section has the whole
468
+ picture.
463
469
 
464
470
  ## Install
465
471
 
@@ -529,8 +535,7 @@ Ruby newer than the release, or a platform with no platform gem. `bundle install
529
535
  Bundler 2.6, not on 3.3's 2.5); otherwise install the compiler and headers. Pinning `fiddle`
530
536
  to your Ruby's version in the `Gemfile.lock` does not stop the build.
531
537
 
532
- Both Windows architectures get a Magnus gem, and the reasoning that once kept them on Fiddle
533
- was backwards: MinGW is the *only* Windows flavour `rb-sys` targets (`x64-mingw-ucrt` and
538
+ Both Windows architectures get a Magnus gem: MinGW is the *only* Windows flavour `rb-sys` targets (`x64-mingw-ucrt` and
534
539
  `aarch64-mingw-ucrt`, both `supported: true` in its own `data/toolchains.json`); the one it
535
540
  has no support for is MSVC. Windows is also where the Fiddle fallback cost the most, so these
536
541
  are the most worthwhile gems in the set. Both extensions are built for the `gnullvm` Rust
@@ -576,6 +581,12 @@ Load `ruby.wasm` with [`@ruby/wasm-wasi`](https://www.npmjs.com/package/@ruby/wa
576
581
  - **No Rust toolchain.** The gem's `extconf.rb` only hands rbwasm the prebuilt archive, and
577
582
  rbwasm downloads its own wasi-sdk. The first `rbwasm build` compiles Ruby itself and takes
578
583
  15–20 minutes; later builds reuse it.
584
+ - **macOS with Homebrew: limit pkg-config to `openssl@3`.** Run the build as
585
+ `PKG_CONFIG_LIBDIR="$(brew --prefix openssl@3)/lib/pkgconfig" bundle exec rbwasm build …`.
586
+ Without it Homebrew's pkg-config files reach into the build twice: its `openssl` is OpenSSL
587
+ 4, which the native Ruby rbwasm builds first does not compile against, and its `libyaml`
588
+ is a macOS library that psych then links into the wasm interpreter, which fails for Ruby
589
+ 4.0.
579
590
  - **`Decimal#to_d` needs `gem "bigdecimal"` in that Gemfile.** bigdecimal is a bundled gem
580
591
  with a C extension, and ruby.wasm builds a bundled gem's extension only when the Gemfile
581
592
  names it; without it, `to_d` raises `LoadError`. `to_s` and `to_r` need nothing.
data/lib/hypercast.rb CHANGED
@@ -30,7 +30,7 @@ require_relative "hypercast/runtime"
30
30
  module HyperCast
31
31
  # This gem's own version — kept in lockstep with hypercast.gemspec by the
32
32
  # prepare-release workflow, so the two can never drift apart again.
33
- VERSION = "0.9.0"
33
+ VERSION = "1.0.0"
34
34
 
35
35
  # The success case of a verdict: a cast value.
36
36
  Success = Data.define(:value)
@@ -60,7 +60,7 @@ module HyperCast
60
60
  # invariant culture, and nothing that parsed before stops parsing. The currency symbol is
61
61
  # honored only while the CURRENCY flag is set — declared with the flag off, the symbol is
62
62
  # a :malformed Fault at the symbol, and the flag with no symbol matches nothing — and the
63
- # percent symbol while PERCENT is. A separator may be one or two characters (", " —
63
+ # percent symbol while PERCENT is. A separator may be wider than one character (", " —
64
64
  # comma, space — is a separator), up to 8 UTF-8 bytes. A malformed format is a caller bug
65
65
  # (ArgumentError), never a verdict: an empty or over-long separator, equal separators (or
66
66
  # an equal pair in force for currency amounts), a symbol carrying an ASCII digit, a sign
@@ -125,7 +125,9 @@ module HyperCast
125
125
  # key (precision, format, significant, ...) ignored — so
126
126
  # `NumFormat.from_i18n(I18n.t("number.format"))` drops a Rails locale straight in, with
127
127
  # no I18n dependency here. A missing or nil key keeps the invariant's "." / "," / no
128
- # symbol. An empty delimiter — the locale writes no grouping — turns GROUPING off rather
128
+ # symbol, except where that would repeat the mark the Hash does declare: a separator of
129
+ # "," with no delimiter groups with ".", and a delimiter of "." with no separator takes
130
+ # "," for the decimal mark. An empty delimiter — the locale writes no grouping — turns GROUPING off rather
129
131
  # than declaring a separator, since a NumFormat always carries one; the placeholder it
130
132
  # gets is whichever of "," and "." the decimal mark is not. A negative_format that writes
131
133
  # the sign after the number ("%n-", "%n %u-") turns TRAILING_SIGN on; "-%n", the Rails
@@ -136,7 +138,9 @@ module HyperCast
136
138
  # differ (the Swiss German franc, say — "1'234.50" beside "1.234,50"): its separator and
137
139
  # delimiter become the currency separators, read in place of the number ones when the
138
140
  # unit is found in the text, and its unit the currency symbol. A missing key there
139
- # declares nothing, keeping the number separator it stands in for; an explicitly empty
141
+ # declares nothing, keeping the number separator it stands in for, unless that would
142
+ # repeat the currency mark it does declare, in which case the missing one is whichever
143
+ # of "," and "." that mark is not; an explicitly empty
140
144
  # delimiter gets the same placeholder rule as above; its negative_format turns
141
145
  # TRAILING_SIGN on the same way. Passed alone, number.currency.format reads as the one
142
146
  # number format, as before.
@@ -149,10 +153,10 @@ module HyperCast
149
153
  # narrow flags: for anything stricter.
150
154
  def self.from_i18n(number_format, currency_format: nil, flags: ALL_STYLES)
151
155
  entries = i18n_entries(number_format)
152
- separator = entries[:separator] || "."
153
- delimiter = entries[:delimiter] || ","
154
- if delimiter == ""
155
- flags &= ~GROUPING
156
+ separator = entries[:separator] || (entries[:delimiter] == "." ? "," : ".")
157
+ delimiter = entries[:delimiter]
158
+ if delimiter.nil? || delimiter == ""
159
+ flags &= ~GROUPING if delimiter == ""
156
160
  delimiter = separator == "," ? "." : ","
157
161
  end
158
162
  flags |= TRAILING_SIGN if trailing_sign?(entries[:negative_format])
@@ -164,6 +168,11 @@ module HyperCast
164
168
  currency_separator = money[:separator] || ""
165
169
  currency_delimiter = money[:delimiter] || ""
166
170
  currency_delimiter = (money[:separator] || separator) == "," ? "." : "," if money[:delimiter] == ""
171
+ if currency_separator.empty? && currency_delimiter == separator
172
+ currency_separator = currency_delimiter == "," ? "." : ","
173
+ elsif currency_delimiter.empty? && currency_separator == delimiter
174
+ currency_delimiter = currency_separator == "," ? "." : ","
175
+ end
167
176
  flags |= TRAILING_SIGN if trailing_sign?(money[:negative_format])
168
177
  end
169
178
  new(decimal_sep: separator, group_sep: delimiter, flags: flags, currency: currency,
@@ -253,7 +262,7 @@ module HyperCast
253
262
  NumFormat::CURRENCY_MAX_BYTES = 16
254
263
 
255
264
  # The widest separator, sign or percent symbol the native ABI carries inline, in UTF-8
256
- # bytes: one or two characters of any script.
265
+ # bytes: enough for every separator a real culture declares (one or two characters).
257
266
  NumFormat::SYMBOL_MAX_BYTES = 8
258
267
 
259
268
  # The size of NumFormat#packed — the native ABI's size-tagged NumFormat layout — in bytes.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hypercast-wasm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Buvinghausen