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 +4 -4
- data/README.md +54 -43
- data/ext/hypercast_native/3.4/hypercast_native.a.gz +0 -0
- data/ext/hypercast_native/4.0/hypercast_native.a.gz +0 -0
- data/lib/hypercast.rb +18 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8fba4b024f4c3f507d9c5d31187c8434d704e8ce0b613a5dd0d8de1100716e8
|
|
4
|
+
data.tar.gz: 9156a91e12b17035fa8e7cca34d79d0990e20388b6037386a1555e2a7f9526a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
150
|
-
|
|
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
|
|
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
|
|
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
|
|
278
|
-
|
|
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.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
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` (
|
|
370
|
-
|
|
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
|
|
381
|
-
|
|
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
|
-
|
|
442
|
-
this
|
|
443
|
-
|
|
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
|
|
447
|
-
gh attestation verify 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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
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.
|
|
Binary file
|
|
Binary file
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|