hypercast 0.3.0-x86_64-linux → 0.4.0-x86_64-linux
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 +254 -74
- data/lib/hypercast/3.4/hypercast_native.so +0 -0
- data/lib/hypercast/4.0/hypercast_native.so +0 -0
- data/lib/hypercast/native/linux-arm64/libhypercast.so +0 -0
- data/lib/hypercast/native/linux-musl-arm64/libhypercast.so +0 -0
- data/lib/hypercast/native/linux-musl-x64/libhypercast.so +0 -0
- data/lib/hypercast/native/linux-x64/libhypercast.so +0 -0
- data/lib/hypercast/native/osx-arm64/libhypercast.dylib +0 -0
- data/lib/hypercast/native/osx-x64/libhypercast.dylib +0 -0
- data/lib/hypercast/native/wasm32-wasip1/hypercast.wasm +0 -0
- data/lib/hypercast/native/win-arm64/hypercast.dll +0 -0
- data/lib/hypercast/native/win-x64/hypercast.dll +0 -0
- data/lib/hypercast/native_platform.rb +13 -7
- data/lib/hypercast.rb +63 -19
- metadata +18 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 39c6dc5065dbf206aa1760bf0385ea8bea8f131f11dea3489d92423614393e13
|
|
4
|
+
data.tar.gz: fbcb0af9091c853eebdb56aa1c85fb229844c52cb9995281396ad004f5500ae5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 74f52267197d0934ff760441a2be4e4435891bb067bb6357f60c378a7a00bc58d52d53307698f9ba2e65e8a380596bbb7c2a32fb6701682f4c4a5335ae60269f
|
|
7
|
+
data.tar.gz: 9701f13462c868a190ab02f3acbd9e43acc71d30c4684a3160c291b22427bbbfaac6b890308468ae4a1283908250cfa494157aa3007aa874f40ce364ef148c74
|
data/README.md
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
# hypercast
|
|
2
2
|
|
|
3
3
|
[](https://github.com/SkunkWerkx/HyperCast/actions/workflows/ci.yml)
|
|
4
|
-
[](https://rubygems.org/gems/hypercast)
|
|
5
5
|
[](https://github.com/SkunkWerkx/HyperCast/blob/master/LICENSE)
|
|
6
6
|
|
|
7
7
|
**Ruby's own pattern matching over two `Data` case types — the value, or a closed reason
|
|
8
|
-
Symbol plus the exact
|
|
8
|
+
Symbol plus the exact span that offended. Three backends, one public surface: a Magnus
|
|
9
9
|
native extension where a precompiled platform gem covers you, stdlib Fiddle everywhere
|
|
10
10
|
else — selected automatically, zero compiles either way — and the same core as a
|
|
11
11
|
WebAssembly module inside the `wasmtime` gem for any platform with no native build at all.**
|
|
12
12
|
|
|
13
13
|
Allocation-lean scalar casts — booleans, the full integer family, reals, exact decimals,
|
|
14
|
-
UUIDs, temporals — calling directly into the native `libhypercast` Rust core. Ruby 3.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
[WebAssembly (wasmtime)](#webassembly-wasmtime).
|
|
14
|
+
UUIDs, temporals — calling directly into the native `libhypercast` Rust core. Ruby 3.3 is the
|
|
15
|
+
floor. The fast path links the core straight into a Ruby extension (Magnus): on require it
|
|
16
|
+
redefines the doors in place on the `HyperCast` module — no delegation layer, no second
|
|
17
|
+
surface, which is exactly what keeps the backends provably in agreement.
|
|
18
|
+
`HyperCast::BACKEND` reports which is live; `HYPERCAST_PURE=1` forces Fiddle,
|
|
19
|
+
`HYPERCAST_WASM=1` the wasm module — see [Backends](#backends).
|
|
21
20
|
|
|
22
21
|
```ruby
|
|
23
22
|
case HyperCast.i32("(1,234)", HyperCast::NumFormat::INVARIANT)
|
|
24
23
|
in HyperCast::Success(value:) then puts "got #{value}" # -1234
|
|
25
|
-
in HyperCast::Fault(reason:, offset:) then puts "#{reason} at
|
|
24
|
+
in HyperCast::Fault(reason:, offset:) then puts "#{reason} at #{offset}"
|
|
26
25
|
end
|
|
27
26
|
```
|
|
28
27
|
|
|
@@ -52,13 +51,43 @@ midnight, durations come back as exact `Rational` seconds across the core's whol
|
|
|
52
51
|
| `duration(text)` | exact `Rational` seconds | — |
|
|
53
52
|
|
|
54
53
|
Every door returns a `Success` or a `Fault`; `HyperCast.optional(verdict)` folds `:empty`
|
|
55
|
-
to `nil`. Beside the doors, `
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
to `nil`. Beside the doors, `native_version` returns the loaded core's
|
|
55
|
+
`"major.minor.patch"` and `available?` answers `true`/`false` without ever raising — see
|
|
56
|
+
[Is the native core there?](#is-the-native-core-there).
|
|
57
|
+
|
|
58
|
+
### Errors
|
|
59
|
+
|
|
60
|
+
Bad data is never an exception — it is a `Fault`. What raises is a caller bug, and it raises
|
|
61
|
+
the same exception on every backend:
|
|
62
|
+
|
|
63
|
+
- `ArgumentError` from `NumFormat.new` for a malformed format: separators that are not single
|
|
64
|
+
characters or not distinct, a currency symbol that is too long or carries an ASCII digit or
|
|
65
|
+
whitespace.
|
|
66
|
+
- `KeyError` — `Hash#fetch`'s own, `key not found: …` — for a precision, epoch or date order
|
|
67
|
+
that names nothing: an unknown Symbol, or anything that is not a Symbol (`"seconds"`
|
|
68
|
+
included). `date(text, nil)` is not one of these: an explicit `nil` order is the strict ISO
|
|
69
|
+
door, exactly as if it were left off.
|
|
70
|
+
- `TypeError` for text that is not a `String` (anything with `to_str` is taken as one).
|
|
71
|
+
- `String#encode`'s own `Encoding::` error for text in another encoding that cannot be
|
|
72
|
+
transcoded to UTF-8.
|
|
73
|
+
|
|
74
|
+
### Is the native core there?
|
|
75
|
+
|
|
76
|
+
`HyperCast.available?` answers whether a backend actually loaded and exports the ABI this
|
|
77
|
+
binding was built against — probed once, cached, never raising — for a consumer with a
|
|
78
|
+
fallback of its own:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
value = HyperCast.available? ? HyperCast.i32(text, format) : my_own_parse(text)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`HyperCast.native_version` reads the version out of the loaded core itself (its
|
|
85
|
+
`hypercast_version` export — the cheapest probe that the backend resolved at all), so a
|
|
86
|
+
mismatch against `HyperCast::VERSION` can be named before the first cast.
|
|
87
|
+
`HyperCast::BACKEND` says which backend was *selected*, which is not the same question: it
|
|
88
|
+
reads `:fiddle` on a platform with no library at all, because that is the backend whose
|
|
89
|
+
first call explains what is missing — a `LoadError` naming the path it looked for, or
|
|
90
|
+
`HyperCast::NativePlatform::UnsupportedPlatformError` naming the platform.
|
|
62
91
|
|
|
63
92
|
### Fault spans index the String you passed
|
|
64
93
|
|
|
@@ -136,15 +165,14 @@ and memoized by identity on every backend, so declaring a currency costs a cast
|
|
|
136
165
|
corpus replay; cross-backend agreement specs compare Magnus against Fiddle
|
|
137
166
|
and wasm against Fiddle across a subprocess boundary).
|
|
138
167
|
4. **Faster than the stdlib on the Magnus backend, where the carrier is cheap** —
|
|
139
|
-
benchmark-ips (`ruby benchmark/cast_benchmark.rb`, linux-
|
|
140
|
-
2.
|
|
141
|
-
duration door. The Fiddle fallback lands at
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Separator detection is free here: `1.234.567,89` under `NumFormat::DETECT` runs
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
with parsing: every format other than `INVARIANT` paid three method dispatches and two
|
|
168
|
+
benchmark-ips (`ruby benchmark/cast_benchmark.rb`, linux-x64 on an Intel Core
|
|
169
|
+
i9-11900H, Ruby 4.0.7): timestamp **447 ns vs 2.97 µs `Time.iso8601`** (6.6x) — while
|
|
170
|
+
returning exact `Rational` durations on the duration door. The Fiddle fallback lands at
|
|
171
|
+
3.25 µs: a little behind `Time.iso8601`, sitting on Fiddle's per-call marshalling floor.
|
|
172
|
+
|
|
173
|
+
Separator detection is nearly free here: `1.234.567,89` under `NumFormat::DETECT` runs
|
|
174
|
+
at 178 ns against 170 ns for the same text under a declared eurozone format. Both cost
|
|
175
|
+
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
|
|
148
176
|
`String` allocations per call to read its separators back out of the `Data`. `DETECT`
|
|
149
177
|
is now identity-matched like `INVARIANT`, and any other format is resolved once per
|
|
150
178
|
thread and memoized by identity — anchored in a thread-variable so the memo's key can
|
|
@@ -153,26 +181,78 @@ and memoized by identity on every backend, so declaring a currency costs a cast
|
|
|
153
181
|
cached the same way, so a fault or a declared option is a pointer compare too, never a
|
|
154
182
|
`Symbol#name` materialization.
|
|
155
183
|
|
|
156
|
-
**The honest trade-off
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
stdlib `DateTime` with an exact `Rational` second costs more than the whole native call,
|
|
160
|
-
where the timestamp door's `Time` is built by a single cheap `rb_time_nano_new`.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
**The honest trade-off:** the civil doors do not beat `strptime`. The date-time door is
|
|
185
|
+
level with `DateTime.strptime` (904 ns against 901 ns) and the date door a little behind
|
|
186
|
+
`Date.strptime` (620 ns against 513 ns). The parse isn't the cost; the carrier is. Building
|
|
187
|
+
a stdlib `DateTime` with an exact `Rational` second costs more than the whole native call,
|
|
188
|
+
where the timestamp door's `Time` is built by a single cheap `rb_time_nano_new`. If you want
|
|
189
|
+
Ruby's fastest civil parse and don't need the verdict or the declared order, `strptime` is
|
|
190
|
+
as good. Also note the carrier's other caveat — `DateTime`'s offset defaults to `+00:00`,
|
|
191
|
+
which is an artifact of the type, not a zone the parse assigned.
|
|
164
192
|
|
|
165
193
|
On the Fiddle fallback the doors are parity-at-best — Fiddle's
|
|
166
194
|
per-call floor is the mechanism's price, kept because it's the universal zero-compile
|
|
167
|
-
path.
|
|
168
|
-
|
|
169
|
-
doors interpolated and interned their `:cast_*` Symbol on every call, every door went
|
|
170
|
-
through a splat-and-resplat dispatcher, and every numeric call copied the format's 12
|
|
171
|
-
packed bytes into scratch — each format now owns one native pointer, memoized by identity,
|
|
172
|
-
passed straight through. (Benchmark forensics worth knowing: the doors read 4.3 µs until
|
|
195
|
+
path. Its doors build nothing per call that does not change between calls: each format
|
|
196
|
+
owns one native pointer, memoized by identity and passed straight through. (Benchmark forensics worth knowing: the doors read 4.3 µs until
|
|
173
197
|
per-call `Fiddle::Pointer.malloc` finalizers were hoisted to thread-local scratch —
|
|
174
198
|
receipts include their own archaeology.)
|
|
175
199
|
|
|
200
|
+
## Benchmarks
|
|
201
|
+
|
|
202
|
+
`ruby benchmark/cast_benchmark.rb` pairs each door with Ruby's closest stdlib parse. Its
|
|
203
|
+
first line names the backend, core version and Ruby it measured, so run it again under
|
|
204
|
+
`HYPERCAST_PURE=1` or `HYPERCAST_WASM=1` for the other two backends. The stdlib comparisons
|
|
205
|
+
are in [the section above](#why-not-integer--timeiso8601--float); this is the three backends
|
|
206
|
+
against each other.
|
|
207
|
+
|
|
208
|
+
Measured on Ruby 4.0.7, linux-x64 (an Intel Core i9-11900H), wasmtime 48.0.1,
|
|
209
|
+
`benchmark-ips`, same session, all three backends:
|
|
210
|
+
|
|
211
|
+
| Door | Magnus | Fiddle | wasmtime |
|
|
212
|
+
|---|---:|---:|---:|
|
|
213
|
+
| `bool` | 112 ns | 2.35 µs | 1.75 µs |
|
|
214
|
+
| `i32` | 133 ns | 2.63 µs | 1.80 µs |
|
|
215
|
+
| `f64` | 166 ns | 2.70 µs | 1.84 µs |
|
|
216
|
+
| `uuid` | 223 ns | 3.47 µs | 2.65 µs |
|
|
217
|
+
| `timestamp` | 447 ns | 3.25 µs | 2.42 µs |
|
|
218
|
+
| `datetime` (`1/7/2026 3:04 PM`) | 904 ns | 3.98 µs | 3.14 µs |
|
|
219
|
+
| `duration` (ISO) | 632 ns | 2.91 µs | 2.10 µs |
|
|
220
|
+
| `i32`, a fault | 225 ns | 3.18 µs | 2.50 µs |
|
|
221
|
+
|
|
222
|
+
A lean door on the Magnus backend is little more than the native call: the extension builds
|
|
223
|
+
the `Success` or `Fault` it returns directly — allocated, its members stored, frozen —
|
|
224
|
+
rather than through `Data.new`, whose keyword handling alone cost more than the cast.
|
|
225
|
+
|
|
226
|
+
## Backends
|
|
227
|
+
|
|
228
|
+
| `HyperCast::BACKEND` | What runs | Chosen when |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| `:native` | the core linked into a Magnus extension | a precompiled platform gem carries an extension for this Ruby's ABI — see [Install](#install) |
|
|
231
|
+
| `:fiddle` | `libhypercast` for this platform, `dlopen`ed through Fiddle | no extension loads; also the answer when nothing loads at all |
|
|
232
|
+
| `:wasm` | the core as a `wasm32-wasip1` module inside the `wasmtime` gem | no extension and no library for this platform, and `wasmtime` is installed — see [WebAssembly (wasmtime)](#webassembly-wasmtime) |
|
|
233
|
+
|
|
234
|
+
Selection happens once, at `require`, in that order. Two environment variables override it:
|
|
235
|
+
|
|
236
|
+
| Variable | Effect |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `HYPERCAST_WASM` | forces `:wasm`; `require` raises a `LoadError` naming the gem if `wasmtime` is missing |
|
|
239
|
+
| `HYPERCAST_PURE` | forces `:fiddle`, even where an extension would load |
|
|
240
|
+
|
|
241
|
+
Both are read for presence, not value — set to anything at all, `0` and the empty string
|
|
242
|
+
included, they force their backend — and `HYPERCAST_WASM` wins when both are set. A forced
|
|
243
|
+
backend that turns out to have nothing to load does not fall through to another one: the
|
|
244
|
+
first call raises, and `HyperCast.available?` answers `false`.
|
|
245
|
+
|
|
246
|
+
**Threads.** Every backend is safe to call from any number of threads. The extension runs
|
|
247
|
+
under the GVL. Fiddle releases the GVL for the duration of each call, so that is the one
|
|
248
|
+
backend where Ruby threads run the core truly in parallel — each through its own scratch
|
|
249
|
+
buffers; the core itself keeps no state between calls. The wasm backend serializes every call
|
|
250
|
+
on one Mutex around one instance. `spec/cast_spec.rb`'s concurrent-callers example runs under
|
|
251
|
+
all three.
|
|
252
|
+
|
|
253
|
+
**Ractors.** Main Ractor only, on every backend: called from another Ractor the doors raise
|
|
254
|
+
`Ractor::UnsafeError` (`Ractor::IsolationError` under wasm).
|
|
255
|
+
|
|
176
256
|
## WebAssembly (wasmtime)
|
|
177
257
|
|
|
178
258
|
The Rust core also ships inside this gem as a `wasm32-wasip1` module
|
|
@@ -210,29 +290,20 @@ with `Memory#read`; using the guest's allocator rather than a host-picked offset
|
|
|
210
290
|
a buffer from being clobbered by the guest's next allocation. And a `Wasmtime::Store` is
|
|
211
291
|
single-threaded, so every call is serialized under one Mutex around one shared instance.
|
|
212
292
|
|
|
213
|
-
Measured
|
|
214
|
-
|
|
293
|
+
Measured against the other two backends in the same session — the `wasmtime` column of the
|
|
294
|
+
table under [Benchmarks](#benchmarks).
|
|
215
295
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
| `datetime` (`1/7/2026 3:04 PM`) | 1.36 µs | 3.97 µs | 3.66 µs |
|
|
224
|
-
| `duration` (ISO) | 975 ns | 2.84 µs | 2.47 µs |
|
|
225
|
-
| `i32`, a fault | 658 ns | 2.89 µs | 2.42 µs |
|
|
226
|
-
|
|
227
|
-
The finding worth stating: in Ruby the wasm backend lands at Fiddle parity, door for door.
|
|
228
|
-
Fiddle's per-call floor is interpreted marshalling; wasmtime's is the host-to-guest crossing
|
|
229
|
-
plus the guest memory copies; on this box they cost the same, and both sit 4-5x behind the
|
|
230
|
-
Magnus extension. So on a platform with no native build, the fallback costs a consumer
|
|
231
|
-
nothing they were not already paying on the universal gem.
|
|
296
|
+
The finding worth stating: in Ruby the wasm backend is no slower than Fiddle, and on the
|
|
297
|
+
x64 box that table comes from it is the faster of the two on every door, by a fifth to a
|
|
298
|
+
third. Fiddle's per-call floor is interpreted marshalling; wasmtime's is the host-to-guest
|
|
299
|
+
crossing plus the guest memory copies; on the lean doors wasmtime sits eleven to sixteen
|
|
300
|
+
times behind the Magnus extension and Fiddle sixteen to twenty-one, narrowing to three to
|
|
301
|
+
five times where building the Ruby value dominates. So on a platform with no native build, the fallback costs a consumer nothing
|
|
302
|
+
they were not already paying on the universal gem.
|
|
232
303
|
|
|
233
304
|
## Verifying provenance
|
|
234
305
|
|
|
235
|
-
Every gem RubyGems.org serves — the universal fallback and each of the
|
|
306
|
+
Every gem RubyGems.org serves — the universal fallback and each of the five precompiled
|
|
236
307
|
platform gems — carries its own GitHub build-provenance attestation, signed directly by
|
|
237
308
|
this repo's own `release.yml` (the `rubygems-publish` job attests `ruby/pkg/*.gem` right
|
|
238
309
|
before the push), so plain `--repo` verifies any of them:
|
|
@@ -243,10 +314,11 @@ gh attestation verify hypercast-X.Y.Z-<platform>.gem --repo SkunkWerkx/HyperCast
|
|
|
243
314
|
```
|
|
244
315
|
|
|
245
316
|
That's the release's second layer of checking, not the only one: before any gem gets built,
|
|
246
|
-
the same job verifies every native binary it packs
|
|
247
|
-
|
|
248
|
-
`SkunkWerkx/.github` by `hyper-build-native.yml`, so
|
|
249
|
-
`--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an
|
|
317
|
+
the same job verifies every native binary it packs — the Fiddle libraries, the Magnus
|
|
318
|
+
extensions (one per Ruby ABI per platform) and the wasm module — against *their own*
|
|
319
|
+
attestations — those are signed from `SkunkWerkx/.github` by `hyper-build-native.yml`, so
|
|
320
|
+
that check needs `--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an
|
|
321
|
+
unverified one.
|
|
250
322
|
RubyGems.org has no unpublish and no duplicate-version overwrite, so this all happens while a
|
|
251
323
|
bad artifact is still reversible. The release run's job summary then re-fetches every gem
|
|
252
324
|
from the CDN and records attested-vs-served digests, turning "rubygems.org stores an upload
|
|
@@ -260,10 +332,15 @@ more on why `--signer-repo` is needed for some artifacts here and not others.
|
|
|
260
332
|
gem install hypercast
|
|
261
333
|
```
|
|
262
334
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
`
|
|
266
|
-
that `gem install` auto-
|
|
335
|
+
Six gems are published per release: one universal `ruby`-platform gem (Fiddle, with every
|
|
336
|
+
platform's native library and the wasm module bundled) plus five precompiled Magnus platform
|
|
337
|
+
gems (`x86_64-linux`, `aarch64-linux`, `arm64-darwin`, `x64-mingw-ucrt`,
|
|
338
|
+
`aarch64-mingw-ucrt`) that `gem install` and `bundle` auto-select when they match. There is
|
|
339
|
+
no `x86_64-darwin` platform gem: an Intel Mac installs the universal gem and runs on Fiddle
|
|
340
|
+
over the bundled `osx-x64` library, as Alpine does. A platform
|
|
341
|
+
gem carries the same libraries and module beside its extensions, so the Fiddle and wasm
|
|
342
|
+
backends are still there behind `HYPERCAST_PURE` and `HYPERCAST_WASM`. No extra configuration
|
|
343
|
+
needed either way.
|
|
267
344
|
|
|
268
345
|
Selection has **two** axes here, unlike every other binding in this repo. A Magnus extension
|
|
269
346
|
is bound to one Ruby minor ABI — there is no `abi3` equivalent to collapse the version axis
|
|
@@ -271,11 +348,19 @@ the way [the Python binding's](../python/) wheels do — so each platform gem is
|
|
|
271
348
|
carrying one compiled extension per supported Ruby, under `lib/hypercast/<minor>/`, and picks
|
|
272
349
|
one at `require` time:
|
|
273
350
|
|
|
274
|
-
| Ruby |
|
|
275
|
-
| --- | --- | --- |
|
|
276
|
-
| 4.0 (primary) | Magnus, `
|
|
277
|
-
| 3.4 (
|
|
278
|
-
| 3.
|
|
351
|
+
| Ruby | glibc Linux, macOS, Windows — x64 and arm64 | musl Linux (Alpine) — x64 and arm64 | anywhere else |
|
|
352
|
+
| --- | --- | --- | --- |
|
|
353
|
+
| 4.0 (primary) | Magnus, `BACKEND == :native` | Fiddle | wasm, if `wasmtime` is installed |
|
|
354
|
+
| 3.4 (until its EOL 2028-03-31) | Magnus, `BACKEND == :native` | Fiddle | wasm, if `wasmtime` is installed |
|
|
355
|
+
| 3.3 (the floor) | Fiddle | Fiddle | wasm, if `wasmtime` is installed |
|
|
356
|
+
|
|
357
|
+
What stands behind each cell: CI replays the whole suite, shared corpus included, for the
|
|
358
|
+
first column on every push — Magnus on Ruby 3.4 and 4.0, Fiddle and wasm on 4.0, on all five
|
|
359
|
+
platforms with a CI leg (Intel macOS has none) — and the Fiddle suite for the musl column inside an Alpine container. The 3.3 row
|
|
360
|
+
is the same Fiddle code path, run with the Docker command under [Development](#development)
|
|
361
|
+
(Ruby 3.3 with the Fiddle 1.1.2 it ships) rather than on every push. The last column — wasm
|
|
362
|
+
chosen automatically because nothing native exists — has no leg of its own: CI runs the wasm
|
|
363
|
+
backend's suite forced, on platforms that have native builds too.
|
|
279
364
|
|
|
280
365
|
The platform gems declare `required_ruby_version >= 3.4, < 4.1` precisely so RubyGems
|
|
281
366
|
*declines* them outside that range and resolves the universal gem instead — a wrong-ABI
|
|
@@ -283,7 +368,27 @@ extension must never be installed in the first place. On Windows it would at lea
|
|
|
283
368
|
load cleanly (the extension imports `<arch>-ucrt-ruby<minor>.dll` by name —
|
|
284
369
|
`x64-ucrt-ruby400.dll` on x64, `aarch64-ucrt-ruby400.dll` on ARM), but Linux extensions don't
|
|
285
370
|
link libruby at all, so one can load successfully against the wrong ABI and misbehave later.
|
|
286
|
-
|
|
371
|
+
When 3.4 goes EOL it simply leaves the matrix and its users fall back to Fiddle, which is
|
|
372
|
+
exactly what the fallback is for.
|
|
373
|
+
|
|
374
|
+
**musl.** RubyGems does not tell musl from glibc for these gems: on Alpine, `gem install`
|
|
375
|
+
picks the `x86_64-linux` or `aarch64-linux` platform gem exactly as it does on any other
|
|
376
|
+
Linux (checked with RubyGems 4.0.20 on `x86_64-linux-musl`). The Magnus extension inside is
|
|
377
|
+
linked against glibc and cannot load there — `require` fails cleanly on the missing
|
|
378
|
+
`ld-linux` loader — so the gem falls back to Fiddle, which loads the musl build of the core
|
|
379
|
+
(`native/linux-musl-x64` or `native/linux-musl-arm64`, chosen from `RUBY_PLATFORM`) that
|
|
380
|
+
every gem carries. Alpine therefore works as installed, on the Fiddle backend and at Fiddle's
|
|
381
|
+
speed. Releases through 0.3.0 carried no musl library: there the fallback found only the
|
|
382
|
+
glibc one, `HyperCast.available?` answered `false`, and the first cast raised
|
|
383
|
+
`Fiddle::DLError`.
|
|
384
|
+
|
|
385
|
+
**Nothing in this gem is ever compiled, on any platform.** Its one dependency can be:
|
|
386
|
+
`fiddle` is a bundled gem on Ruby 4.0 and a default gem on 3.3 and 3.4, and `gem install` is
|
|
387
|
+
satisfied by the copy Ruby ships. Bundler resolves the newest `fiddle` on rubygems.org
|
|
388
|
+
instead, and when that is newer than the one your Ruby ships — true today on 3.3 and 3.4, not
|
|
389
|
+
on 4.0 — it builds Fiddle's own C extension, which takes a compiler and libffi's headers
|
|
390
|
+
(`apk add build-base libffi-dev` on Alpine). Holding `fiddle` at your Ruby's version in the
|
|
391
|
+
`Gemfile.lock` avoids that.
|
|
287
392
|
|
|
288
393
|
Both Windows architectures get a Magnus gem, and the reasoning that once kept them on Fiddle
|
|
289
394
|
was backwards: MinGW is the *only* Windows flavour `rb-sys` targets (`x64-mingw-ucrt` and
|
|
@@ -294,7 +399,82 @@ targets rather than `gnu` — the same mingw-w64/UCRT ABI RubyInstaller's Ruby u
|
|
|
294
399
|
with LLVM and compiler-rt instead of GCC and a statically-linked libgcc, which is what keeps
|
|
295
400
|
the shipped extension small. The build script, and the two flags that are load-bearing on
|
|
296
401
|
the ARM leg (a static libunwind, and a clang-spelled `--target` for bindgen), live in the
|
|
297
|
-
forge's `
|
|
402
|
+
forge's `ruby-magnus` action (`build-magnus.sh`), shared with every other Hyper* repo.
|
|
403
|
+
|
|
404
|
+
## Development
|
|
405
|
+
|
|
406
|
+
Everything below runs from a checkout, with `rust/` and `corpus/` beside `ruby/`; none of it
|
|
407
|
+
is needed to use the gem. The Fiddle and wasm backends find the in-repo builds on their own
|
|
408
|
+
when nothing is staged under `lib/hypercast/native/`. The Magnus backend does not — an
|
|
409
|
+
extension has to be built for the Ruby you are running and put where `require` looks — and
|
|
410
|
+
that is what `rake native:dev` is for.
|
|
411
|
+
|
|
412
|
+
```sh
|
|
413
|
+
cd rust
|
|
414
|
+
cargo build --release # libhypercast, what the Fiddle backend loads
|
|
415
|
+
cargo build --release --target wasm32-wasip1 # hypercast.wasm, what the wasm backend loads
|
|
416
|
+
|
|
417
|
+
cd ../ruby
|
|
418
|
+
bundle install
|
|
419
|
+
bundle exec rake native:dev # build the Magnus extension for this Ruby and stage it
|
|
420
|
+
bundle exec rspec # BACKEND == :native
|
|
421
|
+
HYPERCAST_PURE=1 bundle exec rspec # BACKEND == :fiddle
|
|
422
|
+
HYPERCAST_WASM=1 bundle exec rspec # BACKEND == :wasm
|
|
423
|
+
bundle exec rake docs:check # every public object carries a doc comment
|
|
424
|
+
ruby benchmark/cast_benchmark.rb # prints the backend it measured
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
`rake native:dev` runs `cargo ruby-ext` (an alias in `rust/.cargo/config.toml` that builds the
|
|
428
|
+
`ruby` feature into `rust/target/ruby/`, never over the plain library in
|
|
429
|
+
`rust/target/release/`) and copies the result to
|
|
430
|
+
`lib/hypercast/<minor>/hypercast_native.<so|bundle>` — the path `lib/hypercast.rb` tries
|
|
431
|
+
first. The copy is also a rename, and it is the step `cargo ruby-ext` alone does not do: cargo
|
|
432
|
+
names its output `libhypercast.so`, and Ruby derives the `Init_` function it calls from the
|
|
433
|
+
file name it was asked to `require`. Three things worth knowing:
|
|
434
|
+
|
|
435
|
+
- **Run it again after changing `rust/`.** The staged file is a copy; nothing rebuilds it.
|
|
436
|
+
- **One staging per Ruby.** An extension is bound to one Ruby minor, so under a second Ruby
|
|
437
|
+
(`RBENV_VERSION=3.4.11 bundle exec rake native:dev`, say) the task rebuilds for that ABI
|
|
438
|
+
and stages beside the first, each Ruby loading its own.
|
|
439
|
+
- **Without it, `bundle exec rspec` still passes — on Fiddle.** The examples under "native
|
|
440
|
+
backend" report themselves pending with `BACKEND=fiddle`; that line, or
|
|
441
|
+
`ruby -Ilib -rhypercast -e 'p HyperCast::BACKEND'`, is how to tell which backend a run
|
|
442
|
+
exercised. Deleting `lib/hypercast/<minor>/` goes back to Fiddle.
|
|
443
|
+
|
|
444
|
+
The task covers Linux and macOS. The Windows extensions need the `gnullvm` targets and linker
|
|
445
|
+
flags in the forge's `build-magnus.sh`, which is also what CI uses on every platform.
|
|
446
|
+
|
|
447
|
+
The musl build has no host to run on outside a container. With the core built for musl at
|
|
448
|
+
`rust/target/musl/linux-musl-x64/libhypercast.so`, this runs the Fiddle suite — corpus replay
|
|
449
|
+
included — against it on Alpine, with the checkout mounted read-only:
|
|
450
|
+
|
|
451
|
+
```sh
|
|
452
|
+
docker run --rm -v "$PWD/..":/src:ro ruby:4.0-alpine sh -euc '
|
|
453
|
+
mkdir /work && cp -r /src/ruby /work/ruby && cp -r /src/corpus /work/corpus
|
|
454
|
+
mkdir -p /work/ruby/lib/hypercast/native/linux-musl-x64
|
|
455
|
+
cp /src/rust/target/musl/linux-musl-x64/libhypercast.so /work/ruby/lib/hypercast/native/linux-musl-x64/
|
|
456
|
+
cd /work/ruby && rm -f Gemfile.lock
|
|
457
|
+
BUNDLE_WITHOUT=wasm bundle install --quiet --prefer-local
|
|
458
|
+
HYPERCAST_PURE=1 BUNDLE_WITHOUT=wasm bundle exec rspec'
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The floor's test is the same container on `ruby:3.3-alpine`. Bundler would compile a newer
|
|
462
|
+
Fiddle there (see [Install](#install)), so this one goes around Bundler and runs against the
|
|
463
|
+
Fiddle that Ruby 3.3 ships:
|
|
464
|
+
|
|
465
|
+
```sh
|
|
466
|
+
docker run --rm -v "$PWD/..":/src:ro ruby:3.3-alpine sh -euc '
|
|
467
|
+
mkdir /work && cp -r /src/ruby /work/ruby && cp -r /src/corpus /work/corpus
|
|
468
|
+
mkdir -p /work/ruby/lib/hypercast/native/linux-musl-x64
|
|
469
|
+
cp /src/rust/target/musl/linux-musl-x64/libhypercast.so /work/ruby/lib/hypercast/native/linux-musl-x64/
|
|
470
|
+
cd /work/ruby && rm -f Gemfile Gemfile.lock
|
|
471
|
+
gem install rspec -v "~> 3.13" --no-document --silent
|
|
472
|
+
HYPERCAST_PURE=1 rspec'
|
|
473
|
+
```
|
|
298
474
|
|
|
299
475
|
See [the repo root README](../README.md) for the full door table, the receipts, and the
|
|
300
476
|
state of every other language binding.
|
|
477
|
+
|
|
478
|
+
## License
|
|
479
|
+
|
|
480
|
+
[MIT](https://github.com/SkunkWerkx/HyperCast/blob/master/LICENSE)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -4,18 +4,24 @@ module HyperCast
|
|
|
4
4
|
module NativePlatform
|
|
5
5
|
class UnsupportedPlatformError < StandardError; end
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# +platform+ is a parameter only so the specs can walk the whole table from one host;
|
|
8
|
+
# every real caller takes the default.
|
|
9
|
+
def self.rid_and_library_name(platform = RUBY_PLATFORM)
|
|
10
|
+
arch = platform.match?(/arm64|aarch64/) ? "arm64" : "x64"
|
|
9
11
|
|
|
10
|
-
case
|
|
12
|
+
case platform
|
|
11
13
|
when /mingw|mswin|windows/
|
|
12
|
-
|
|
14
|
+
["win-#{arch}", "hypercast.dll"]
|
|
13
15
|
when /darwin/
|
|
14
|
-
|
|
16
|
+
["osx-#{arch}", "libhypercast.dylib"]
|
|
15
17
|
when /linux/
|
|
16
|
-
|
|
18
|
+
# Two C libraries, two builds: a glibc-linked library cannot be relied on to dlopen
|
|
19
|
+
# into a musl process (Alpine), so musl gets RIDs of its own. Ruby names the libc
|
|
20
|
+
# in its platform string there ("x86_64-linux-musl") and leaves it off on glibc.
|
|
21
|
+
os = platform.include?("musl") ? "linux-musl" : "linux"
|
|
22
|
+
["#{os}-#{arch}", "libhypercast.so"]
|
|
17
23
|
else
|
|
18
|
-
raise UnsupportedPlatformError, "hypercast: unsupported platform RUBY_PLATFORM=#{
|
|
24
|
+
raise UnsupportedPlatformError, "hypercast: unsupported platform RUBY_PLATFORM=#{platform}"
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
end
|
data/lib/hypercast.rb
CHANGED
|
@@ -3,16 +3,21 @@ require_relative "hypercast/native_platform"
|
|
|
3
3
|
require_relative "hypercast/runtime"
|
|
4
4
|
|
|
5
5
|
# Allocation-lean scalar casts — booleans, numerics, exact decimals, UUIDs, temporals —
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# from one Rust core, reached through whichever of three backends this install can load:
|
|
7
|
+
# the core linked straight into a Magnus native extension (shipped precompiled in the
|
|
8
|
+
# platform gems), the native libhypercast shared library called through Fiddle (the
|
|
9
|
+
# universal gem bundles one per supported platform), or the same core as a WebAssembly
|
|
10
|
+
# module inside the wasmtime gem. HyperCast::BACKEND names the one that loaded; the
|
|
11
|
+
# selection logic is at the bottom of this file. Every door returns a verdict: Success or
|
|
12
|
+
# Fault (a closed reason plus the offending span), never an exception for bad data — the
|
|
13
|
+
# only exceptions here are caller bugs (a malformed NumFormat, an undeclared option, text
|
|
14
|
+
# that is not a String), never data, and the same exception on every backend.
|
|
10
15
|
#
|
|
11
16
|
# Consume with Ruby's own pattern matching over the two Data case types:
|
|
12
17
|
#
|
|
13
18
|
# case HyperCast.i32("(1,234)", HyperCast::NumFormat::INVARIANT)
|
|
14
19
|
# in HyperCast::Success(value:) then puts "got #{value}" # -1234
|
|
15
|
-
# in HyperCast::Fault(reason:, offset:) then puts "#{reason} at
|
|
20
|
+
# in HyperCast::Fault(reason:, offset:) then puts "#{reason} at #{offset}"
|
|
16
21
|
# end
|
|
17
22
|
#
|
|
18
23
|
# Door names mirror the native ABI (i32, f64, timestamp, ...) so the polyglot surface reads
|
|
@@ -25,7 +30,7 @@ require_relative "hypercast/runtime"
|
|
|
25
30
|
module HyperCast
|
|
26
31
|
# This gem's own version — kept in lockstep with hypercast.gemspec by the
|
|
27
32
|
# prepare-release workflow, so the two can never drift apart again.
|
|
28
|
-
VERSION = "0.
|
|
33
|
+
VERSION = "0.4.0"
|
|
29
34
|
|
|
30
35
|
# The success case of a verdict: a cast value.
|
|
31
36
|
Success = Data.define(:value)
|
|
@@ -50,25 +55,29 @@ module HyperCast
|
|
|
50
55
|
# 16 UTF-8 bytes or carrying an ASCII digit or ASCII whitespace, are a caller bug
|
|
51
56
|
# (ArgumentError), never a verdict.
|
|
52
57
|
NumFormat = Data.define(:decimal_sep, :group_sep, :flags, :currency) do
|
|
53
|
-
# The widest currency symbol the native ABI carries inline, in UTF-8 bytes.
|
|
54
|
-
CURRENCY_MAX_BYTES = 16
|
|
55
|
-
|
|
56
58
|
# Validates the declared separators up front — single characters, and distinct from
|
|
57
59
|
# each other — and the currency symbol (a String of at most 16 UTF-8 bytes with no
|
|
58
60
|
# ASCII digit or ASCII whitespace, since those would collide with the digit scan and
|
|
59
61
|
# the trimming around the symbol), so a malformed format fails loudly as the caller bug
|
|
60
|
-
# it is.
|
|
62
|
+
# it is. All three are stored transcoded to UTF-8, the encoding the core reads — a
|
|
63
|
+
# separator handed over in another encoding (a no-break space read from a Latin-1
|
|
64
|
+
# file, say) is the same code point on every backend rather than whatever its bytes
|
|
65
|
+
# happened to spell in the encoding it arrived in.
|
|
61
66
|
def initialize(decimal_sep:, group_sep:, flags:, currency: "")
|
|
62
67
|
raise ArgumentError, "separators must be single characters" unless
|
|
63
68
|
decimal_sep.is_a?(String) && decimal_sep.length == 1 &&
|
|
64
69
|
group_sep.is_a?(String) && group_sep.length == 1
|
|
70
|
+
|
|
71
|
+
decimal_sep = decimal_sep.encode(Encoding::UTF_8)
|
|
72
|
+
group_sep = group_sep.encode(Encoding::UTF_8)
|
|
65
73
|
raise ArgumentError, "decimal and group separators must differ; both are #{decimal_sep.inspect}" if
|
|
66
74
|
decimal_sep == group_sep
|
|
67
75
|
raise ArgumentError, "currency symbol must be a String; got #{currency.inspect}" unless currency.is_a?(String)
|
|
68
76
|
|
|
69
77
|
symbol = currency.encode(Encoding::UTF_8)
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
limit = self.class::CURRENCY_MAX_BYTES
|
|
79
|
+
raise ArgumentError, "currency symbol #{currency.inspect} exceeds #{limit} UTF-8 bytes" if
|
|
80
|
+
symbol.bytesize > limit
|
|
72
81
|
raise ArgumentError, "currency symbol #{currency.inspect} must not contain an ASCII digit or whitespace" if
|
|
73
82
|
symbol.match?(/[0-9\t\n\f\r ]/)
|
|
74
83
|
|
|
@@ -83,6 +92,11 @@ module HyperCast
|
|
|
83
92
|
end
|
|
84
93
|
end
|
|
85
94
|
|
|
95
|
+
# The widest currency symbol the native ABI carries inline, in UTF-8 bytes. Assigned out
|
|
96
|
+
# here rather than inside the Data.define block above: a constant written in that block
|
|
97
|
+
# is scoped lexically, and landed on HyperCast instead of on NumFormat.
|
|
98
|
+
NumFormat::CURRENCY_MAX_BYTES = 16
|
|
99
|
+
|
|
86
100
|
# Permit the group separator between digits (sizes not validated — between digits is the rule).
|
|
87
101
|
GROUPING = 1
|
|
88
102
|
# Permit accounting parentheses as negation: (1,234) is -1234.
|
|
@@ -351,9 +365,19 @@ module HyperCast
|
|
|
351
365
|
BYTE_COMPATIBLE = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::ASCII_8BIT].freeze
|
|
352
366
|
|
|
353
367
|
# Presents the input as UTF-8 bytes: already-compatible text crosses as-is (Fiddle
|
|
354
|
-
# passes a String's bytes for void*
|
|
355
|
-
#
|
|
368
|
+
# passes a String's own bytes for void* — no copy of them); only foreign encodings pay
|
|
369
|
+
# a transcode, and text that cannot be transcoded raises String#encode's own
|
|
370
|
+
# Encoding:: error, the same one the Magnus extension raises through the same call.
|
|
371
|
+
#
|
|
372
|
+
# Text that is not a String is a caller bug, and the same TypeError on every backend:
|
|
373
|
+
# the extension takes its argument through Ruby's implicit String conversion (to_str,
|
|
374
|
+
# or a TypeError), so this does too.
|
|
356
375
|
def utf8(text)
|
|
376
|
+
unless text.is_a?(String)
|
|
377
|
+
converted = String.try_convert(text) or
|
|
378
|
+
raise TypeError, "no implicit conversion of #{text.class} into String"
|
|
379
|
+
text = converted
|
|
380
|
+
end
|
|
357
381
|
BYTE_COMPATIBLE.include?(text.encoding) ? text : text.encode(Encoding::UTF_8)
|
|
358
382
|
end
|
|
359
383
|
|
|
@@ -437,13 +461,25 @@ module HyperCast
|
|
|
437
461
|
Runtime.function(:hypercast_version).call
|
|
438
462
|
end
|
|
439
463
|
|
|
464
|
+
# How many formats packed_cache remembers before it starts forgetting the oldest. Far
|
|
465
|
+
# more than a process that reuses its formats ever holds; what it bounds is the one
|
|
466
|
+
# that builds a NumFormat per request.
|
|
467
|
+
PACKED_CACHE_LIMIT = 64
|
|
468
|
+
|
|
440
469
|
# Identity-keyed memo (compare_by_identity — a pointer hash, not Data's structural
|
|
441
470
|
# #hash over three Strings and an Integer) of a native 32-byte RawNumFormat per format
|
|
442
471
|
# object, filled once from NumFormat#packed. Formats are reused constants in practice,
|
|
443
472
|
# so the common call finds its pointer in one lookup and packs nothing. The Hash holds
|
|
444
473
|
# the format, so a key can never be a recycled address; the race is benign (idempotent).
|
|
474
|
+
#
|
|
475
|
+
# Bounded, oldest entry out first (a Hash keeps insertion order): unbounded, it held
|
|
476
|
+
# every format ever cast with and its 32-byte allocation for the life of the process,
|
|
477
|
+
# which a consumer building a format per request experienced as a leak. A format
|
|
478
|
+
# evicted and then used again is simply packed again, and an evicted pointer stays
|
|
479
|
+
# alive for any call already holding it — the caller's own reference outlives the cache's.
|
|
445
480
|
def packed_cache
|
|
446
481
|
@packed_cache ||= Hash.new do |cache, format|
|
|
482
|
+
cache.shift while cache.size >= PACKED_CACHE_LIMIT
|
|
447
483
|
pointer = Fiddle::Pointer.malloc(32, Fiddle::RUBY_FREE)
|
|
448
484
|
pointer[0, 32] = format.packed
|
|
449
485
|
cache[format] = pointer
|
|
@@ -470,6 +506,10 @@ end
|
|
|
470
506
|
# forces it (and fails loudly if wasmtime is missing); otherwise it is only ever chosen when
|
|
471
507
|
# there is no native library for this platform at all and wasmtime happens to be available,
|
|
472
508
|
# so no supported platform's behavior changes by its existence.
|
|
509
|
+
#
|
|
510
|
+
# Both variables are read for presence, not value — set to anything at all, "0" and the
|
|
511
|
+
# empty string included, they force their backend — and HYPERCAST_WASM wins when both are
|
|
512
|
+
# set.
|
|
473
513
|
HyperCast::BACKEND =
|
|
474
514
|
if ENV["HYPERCAST_WASM"]
|
|
475
515
|
begin
|
|
@@ -486,9 +526,12 @@ HyperCast::BACKEND =
|
|
|
486
526
|
# Two layouts, and both have to work. A released platform gem is a "fat" gem carrying one
|
|
487
527
|
# extension per supported Ruby ABI under lib/hypercast/<minor>/ (see the Rakefile's
|
|
488
528
|
# native:gem task for why an ABI-per-file is unavoidable — Magnus has no `abi3`
|
|
489
|
-
# equivalent)
|
|
490
|
-
#
|
|
491
|
-
#
|
|
529
|
+
# equivalent), and `rake native:dev` — the local dev loop — stages its own build at that
|
|
530
|
+
# same versioned path. CI's in-job staging additionally drops a single extension flat at
|
|
531
|
+
# lib/, which is also where a hand copy of `cargo ruby-ext`'s output goes (the alias only
|
|
532
|
+
# builds into rust/target/ruby/release/; nothing but that rake task copies it here).
|
|
533
|
+
# Trying the versioned path first and the flat one second means neither has to know the
|
|
534
|
+
# other exists.
|
|
492
535
|
#
|
|
493
536
|
# A miss on both is not an error: it means this Ruby/platform combination has no
|
|
494
537
|
# precompiled extension, which is precisely what the Fiddle backend is for.
|
|
@@ -505,8 +548,9 @@ HyperCast::BACKEND =
|
|
|
505
548
|
else
|
|
506
549
|
# No shared library for this platform either. wasmtime, if the consumer has it,
|
|
507
550
|
# is the only backend left that can run here; without it, stay on Fiddle so the
|
|
508
|
-
# first call raises its own precise "not found" LoadError
|
|
509
|
-
#
|
|
551
|
+
# first call raises its own precise error — a "not found" LoadError naming the
|
|
552
|
+
# path, or NativePlatform::UnsupportedPlatformError naming the platform — rather
|
|
553
|
+
# than a vaguer one from here. HyperCast.available? is the quiet way to ask.
|
|
510
554
|
begin
|
|
511
555
|
require "wasmtime"
|
|
512
556
|
require_relative "hypercast/wasm_runtime"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hypercast
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Buvinghausen
|
|
@@ -51,12 +51,14 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0.9'
|
|
54
|
-
description:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
description: |
|
|
55
|
+
Booleans, numerics, exact decimals, UUIDs, and temporals cast from untrusted text by a
|
|
56
|
+
native Rust core — every parse returns a verdict (the value, or a reason plus the
|
|
57
|
+
offending span), never an exception for bad data. Three backends behind one surface,
|
|
58
|
+
selected automatically and all shipped prebuilt: a Magnus extension where a precompiled
|
|
59
|
+
platform gem matches, stdlib Fiddle everywhere else, and the same core as a WebAssembly
|
|
60
|
+
module for anyone who installs the wasmtime gem. No runtime bridge, no dependencies
|
|
61
|
+
beyond Fiddle.
|
|
60
62
|
executables: []
|
|
61
63
|
extensions: []
|
|
62
64
|
extra_rdoc_files: []
|
|
@@ -67,6 +69,8 @@ files:
|
|
|
67
69
|
- lib/hypercast/3.4/hypercast_native.so
|
|
68
70
|
- lib/hypercast/4.0/hypercast_native.so
|
|
69
71
|
- lib/hypercast/native/linux-arm64/libhypercast.so
|
|
72
|
+
- lib/hypercast/native/linux-musl-arm64/libhypercast.so
|
|
73
|
+
- lib/hypercast/native/linux-musl-x64/libhypercast.so
|
|
70
74
|
- lib/hypercast/native/linux-x64/libhypercast.so
|
|
71
75
|
- lib/hypercast/native/osx-arm64/libhypercast.dylib
|
|
72
76
|
- lib/hypercast/native/osx-x64/libhypercast.dylib
|
|
@@ -81,6 +85,10 @@ licenses:
|
|
|
81
85
|
- MIT
|
|
82
86
|
metadata:
|
|
83
87
|
source_code_uri: https://github.com/SkunkWerkx/HyperCast
|
|
88
|
+
changelog_uri: https://github.com/SkunkWerkx/HyperCast/blob/master/CHANGELOG.md
|
|
89
|
+
bug_tracker_uri: https://github.com/SkunkWerkx/HyperCast/issues
|
|
90
|
+
documentation_uri: https://github.com/SkunkWerkx/HyperCast/tree/master/ruby#readme
|
|
91
|
+
rubygems_mfa_required: 'true'
|
|
84
92
|
rdoc_options: []
|
|
85
93
|
require_paths:
|
|
86
94
|
- lib
|
|
@@ -98,8 +106,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
98
106
|
- !ruby/object:Gem::Version
|
|
99
107
|
version: '0'
|
|
100
108
|
requirements: []
|
|
101
|
-
rubygems_version: 4.0.
|
|
109
|
+
rubygems_version: 4.0.20
|
|
102
110
|
specification_version: 4
|
|
103
|
-
summary:
|
|
104
|
-
|
|
111
|
+
summary: Scalar parsing as Success/Fault verdicts over a native Rust core, shipped
|
|
112
|
+
prebuilt
|
|
105
113
|
test_files: []
|