hypercast 0.1.0-aarch64-linux → 0.2.0-aarch64-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4d1833782f7f522eccac5ec7b42093b3938f370177088898b2ad8dd4de75e9c
4
- data.tar.gz: a8510c912cd6a1ec2d89afac1dc726173080594fd7d59cbc9b24b37cbb063dab
3
+ metadata.gz: 4413f5f3ec544107c724ad495a86ca78f0b5d88c06caacbece6c7e5c75b39746
4
+ data.tar.gz: d5e49cb6a3bacdbbeeabafe78954bea6de0ead9973fb22e945339a5c3f19ed97
5
5
  SHA512:
6
- metadata.gz: 43e9c8810af57e464d3dc6ea93aac23d19221a32da79029b030866a52784a3dd466865c99644e98b68a3537230baf307fe0db579b7c748934b9a897e0dfe9341
7
- data.tar.gz: df57134e54feea30a56ad2f8ed614493f14d077f9cb51e9d585be3758ad0a8b0d56e90091439bbcd3adee1886dfb781154b5b012ec43f5abf448d8732d1244b2
6
+ metadata.gz: 05ce5c94b6f3f1ccfd0e9d40bfcb6710acb0b5332141703519a8b74a244d273d9948cf8048873c80fcc475adfaa9c5c85c421559044fa653f6f4343440ea456b
7
+ data.tar.gz: fbec449055ddc9b1d0cee98b325e1287feedf493f4c69d3b76a3ba3c99e6588c38349b0da343ebd7561a20e2993d924e13a7b582aa0026a19d5ca93db7ac5a50
data/README.md CHANGED
@@ -48,9 +48,17 @@ midnight, and durations come back as exact `Rational` seconds across the core's
48
48
  duration door. The Fiddle fallback lands at ~3.5 µs: parity with `Time.iso8601`, sitting
49
49
  on Fiddle's measured 1.6 µs per-call marshalling floor.
50
50
 
51
- Separator detection is free here: `1.234.567,89` under `NumFormat::DETECT` runs
52
- 1.073M i/s against 1.092M i/s for the same text under a declared eurozone format —
53
- inside the error bars.
51
+ Separator detection is free here: `1.234.567,89` under `NumFormat::DETECT` runs at
52
+ 591 ns against 570 ns for the same text under a declared eurozone format — inside the
53
+ error bars. Both of those were ~990 ns in 0.1.0, for a reason that had nothing to do
54
+ with parsing: every format other than `INVARIANT` paid three method dispatches and two
55
+ `String` allocations per call to read its separators back out of the `Data`. `DETECT`
56
+ is now identity-matched like `INVARIANT`, and any other format is resolved once per
57
+ thread and memoized by identity — anchored in a thread-variable so the memo's key can
58
+ never be a recycled address — which turned the per-call cost into one pointer compare.
59
+ The three reason Symbols and the option Symbols (`:seconds`, `:month_day_year`, …) are
60
+ cached the same way, so a fault or a declared option is a pointer compare too, never a
61
+ `Symbol#name` materialization.
54
62
 
55
63
  **The honest trade-off, and Ruby's one real loss:** the civil date-time door is *slower*
56
64
  than `strptime` — 1.30 µs against `DateTime.strptime`'s 1.02 µs, and the date door 1.04 µs
@@ -63,9 +71,38 @@ defaults to `+00:00`, which is an artifact of the type, not a zone the parse ass
63
71
 
64
72
  On the Fiddle fallback the doors are parity-at-best — Fiddle's
65
73
  per-call floor is the mechanism's price, kept because it's the universal zero-compile
66
- path. (Benchmark forensics worth knowing: the doors read 4.3 µs until per-call
67
- `Fiddle::Pointer.malloc` finalizers were hoisted to thread-local scratch receipts
68
- include their own archaeology.)
74
+ path. 0.2.0 still took ~20% off its numeric doors (`i32` 3.37 → 2.62 µs, `f64` 3.31 →
75
+ 2.76 µs, same session) by not building things per call that never changed: the integer
76
+ doors interpolated and interned their `:cast_*` Symbol on every call, every door went
77
+ through a splat-and-resplat dispatcher, and every numeric call copied the format's 12
78
+ packed bytes into scratch — each format now owns one native pointer, memoized by identity,
79
+ passed straight through. (Benchmark forensics worth knowing: the doors read 4.3 µs until
80
+ per-call `Fiddle::Pointer.malloc` finalizers were hoisted to thread-local scratch —
81
+ receipts include their own archaeology.)
82
+
83
+ ## Verifying provenance
84
+
85
+ Every gem RubyGems.org serves — the universal fallback and each of the six precompiled
86
+ platform gems — carries its own GitHub build-provenance attestation, signed directly by
87
+ this repo's own `release.yml` (the `rubygems-publish` job attests `ruby/pkg/*.gem` right
88
+ before the push), so plain `--repo` verifies any of them:
89
+
90
+ ```sh
91
+ gem fetch hypercast -v X.Y.Z --platform <platform> # or omit --platform for the universal gem
92
+ gh attestation verify hypercast-X.Y.Z-<platform>.gem --repo SkunkWerkx/HyperCast
93
+ ```
94
+
95
+ That's the release's second layer of checking, not the only one: before any gem gets built,
96
+ the same job verifies every native binary it packs (six FFI libs, twelve Magnus extensions —
97
+ two ABIs per platform) against *their own* attestations — those are signed from
98
+ `SkunkWerkx/.github` by `hyper-build-native.yml`, so that check needs
99
+ `--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an unverified one.
100
+ RubyGems.org has no unpublish and no duplicate-version overwrite, so this all happens while a
101
+ bad artifact is still reversible. The release run's job summary then re-fetches every gem
102
+ from the CDN and records attested-vs-served digests, turning "rubygems.org stores an upload
103
+ verbatim" into a per-release measurement rather than an assumption — see
104
+ [csharp/README.md's provenance section](../csharp/README.md#native-binary-provenance) for
105
+ more on why `--signer-repo` is needed for some artifacts here and not others.
69
106
 
70
107
  ## Install
71
108
 
@@ -73,11 +110,41 @@ include their own archaeology.)
73
110
  gem install hypercast
74
111
  ```
75
112
 
76
- Five gems are published per release: one universal `ruby`-platform gem (pure Fiddle, all six
77
- platforms' natives bundled) plus four precompiled Magnus platform gems (`x86_64-linux`,
78
- `aarch64-linux`, `x86_64-darwin`, `arm64-darwin`) that `gem install` auto-selects when they
79
- match. Nobody ever compiles anything; Windows and any unmatched platform land on the
80
- universal gem's Fiddle backend, which replays the same corpus green.
113
+ Seven gems are published per release: one universal `ruby`-platform gem (pure Fiddle, all
114
+ six platforms' natives bundled) plus six precompiled Magnus platform gems (`x86_64-linux`,
115
+ `aarch64-linux`, `x86_64-darwin`, `arm64-darwin`, `x64-mingw-ucrt`, `aarch64-mingw-ucrt`)
116
+ that `gem install` auto-selects when they match. Nobody ever compiles anything.
117
+
118
+ Selection has **two** axes here, unlike every other binding in this repo. A Magnus extension
119
+ is bound to one Ruby minor ABI — there is no `abi3` equivalent to collapse the version axis
120
+ the way [the Python binding's](../python/) wheels do — so each platform gem is a "fat" gem
121
+ carrying one compiled extension per supported Ruby, under `lib/hypercast/<minor>/`, and picks
122
+ one at `require` time:
123
+
124
+ | Ruby | the six platforms above | anywhere else (musl/Alpine, …) |
125
+ | --- | --- | --- |
126
+ | 4.0 (primary) | Magnus, `backend: :native` | Fiddle |
127
+ | 3.4 (floor, until its EOL 2028-03-31) | Magnus, `backend: :native` | Fiddle |
128
+ | 3.2 / 3.3 | Fiddle | Fiddle |
129
+
130
+ The platform gems declare `required_ruby_version >= 3.4, < 4.1` precisely so RubyGems
131
+ *declines* them outside that range and resolves the universal gem instead — a wrong-ABI
132
+ extension must never be installed in the first place. On Windows it would at least fail to
133
+ load cleanly (the extension imports `<arch>-ucrt-ruby<minor>.dll` by name —
134
+ `x64-ucrt-ruby400.dll` on x64, `aarch64-ucrt-ruby400.dll` on ARM), but Linux extensions don't
135
+ link libruby at all, so one can load successfully against the wrong ABI and misbehave later.
136
+ Every cell in that table replays the same corpus green.
137
+
138
+ Both Windows architectures get a Magnus gem, and the reasoning that once kept them on Fiddle
139
+ was backwards: MinGW is the *only* Windows flavour `rb-sys` targets (`x64-mingw-ucrt` and
140
+ `aarch64-mingw-ucrt`, both `supported: true` in its own `data/toolchains.json`); the one it
141
+ has no support for is MSVC. Windows is also where the Fiddle fallback cost the most, so these
142
+ are the most worthwhile gems in the set. Both extensions are built for the `gnullvm` Rust
143
+ targets rather than `gnu` — the same mingw-w64/UCRT ABI RubyInstaller's Ruby uses, linked
144
+ with LLVM and compiler-rt instead of GCC and a statically-linked libgcc, which is what keeps
145
+ the shipped extension small. The build script, and the two flags that are load-bearing on
146
+ the ARM leg (a static libunwind, and a clang-spelled `--target` for bindgen), live in the
147
+ forge's `hyper-build-native.yml`, shared with every other Hyper* repo.
81
148
 
82
149
  See [the repo root README](../README.md) for the full door table, the receipts, and the
83
150
  state of every other language binding.
@@ -29,8 +29,10 @@ module HyperCast
29
29
  @functions = nil
30
30
 
31
31
  class << self
32
- def call(symbol, *args)
33
- functions.fetch(symbol).call(*args)
32
+ # The door's Fiddle::Function, for the caller to invoke directly — a splat-through
33
+ # `call(symbol, *args)` here built and re-splatted an Array on every cast.
34
+ def function(symbol)
35
+ functions.fetch(symbol)
34
36
  end
35
37
 
36
38
  private
data/lib/hypercast.rb CHANGED
@@ -23,7 +23,7 @@ require_relative "hypercast/runtime"
23
23
  module HyperCast
24
24
  # This gem's own version — kept in lockstep with hypercast.gemspec by the
25
25
  # prepare-release workflow, so the two can never drift apart again.
26
- VERSION = "0.1.0"
26
+ VERSION = "0.2.0"
27
27
 
28
28
  # The success case of a verdict: a cast value.
29
29
  Success = Data.define(:value)
@@ -120,11 +120,14 @@ module HyperCast
120
120
  .each do |door, unpack|
121
121
  sizes = { "c" => 1, "C" => 1, "s<" => 2, "S<" => 2, "l<" => 4, "L<" => 4, "q<" => 8, "Q<" => 8 }
122
122
  size = sizes.fetch(unpack)
123
+ # Resolved once here, not inside the method: interpolating a Symbol per call built a
124
+ # String and interned it on every integer cast.
125
+ symbol = :"cast_#{door}"
123
126
  # Integer doors: the target type's own range, declared grouping, accounting parens,
124
127
  # non-negative exponent, and 0x/&H/0b two's-complement radix prefixes. Ruby Integer
125
128
  # is unbounded, so u64 comes back as the true unsigned value.
126
129
  define_method(door) do |text, format|
127
- numeric(:"cast_#{door}", text, format, size) { |out| out.unpack1(unpack) }
130
+ numeric(symbol, text, format, size) { |out| out.unpack1(unpack) }
128
131
  end
129
132
  end
130
133
 
@@ -161,7 +164,7 @@ module HyperCast
161
164
  code = UNIX_PRECISIONS.fetch(precision)
162
165
  bytes = utf8(text)
163
166
  out, fault, = scratch
164
- rc = Runtime.call(:cast_unix, input_ptr(bytes), bytes.bytesize, code, out, fault)
167
+ rc = Runtime.function(:cast_unix).call(input_ptr(bytes), bytes.bytesize, code, out, fault)
165
168
  verdict(rc, fault) { instant(out[0, 16]) }
166
169
  end
167
170
 
@@ -179,7 +182,7 @@ module HyperCast
179
182
  code = EXCEL_EPOCHS.fetch(epoch)
180
183
  bytes = utf8(text)
181
184
  out, fault, = scratch
182
- rc = Runtime.call(:cast_excel_serial, input_ptr(bytes), bytes.bytesize, code, out, fault)
185
+ rc = Runtime.function(:cast_excel_serial).call(input_ptr(bytes), bytes.bytesize, code, out, fault)
183
186
  verdict(rc, fault) { instant(out[0, 16]) }
184
187
  end
185
188
 
@@ -198,7 +201,7 @@ module HyperCast
198
201
  code = DATE_ORDERS.fetch(order)
199
202
  bytes = utf8(text)
200
203
  out, fault, = scratch
201
- rc = Runtime.call(:cast_date_ordered, input_ptr(bytes), bytes.bytesize, code, out, fault)
204
+ rc = Runtime.function(:cast_date_ordered).call(input_ptr(bytes), bytes.bytesize, code, out, fault)
202
205
  verdict(rc, fault) do
203
206
  year, month, day = out[0, 4].unpack("S<CC")
204
207
  Date.new(year, month, day)
@@ -218,7 +221,7 @@ module HyperCast
218
221
  code = DATE_ORDERS.fetch(order)
219
222
  bytes = utf8(text)
220
223
  out, fault, = scratch
221
- rc = Runtime.call(:cast_datetime, input_ptr(bytes), bytes.bytesize, code, out, fault)
224
+ rc = Runtime.function(:cast_datetime).call(input_ptr(bytes), bytes.bytesize, code, out, fault)
222
225
  verdict(rc, fault) do
223
226
  year, month, day, nanos = out[0, 16].unpack("S<CCx4Q<")
224
227
  second_of_day, frac = nanos.divmod(1_000_000_000)
@@ -261,14 +264,16 @@ module HyperCast
261
264
  bytes.empty? ? nil : bytes
262
265
  end
263
266
 
264
- # One 36-byte scratch allocation per thread, reused by every call: out-value at 0
265
- # (16 bytes covers every door), fault span at 16, NumFormat at 24. Two
266
- # Fiddle::Pointer.malloc(..., RUBY_FREE) calls per cast — each registering a GC
267
- # finalizer — measured as the dominant per-call cost by an order of magnitude.
267
+ # One 24-byte scratch allocation per thread, reused by every call: out-value at 0
268
+ # (16 bytes covers every door), fault span at 16. Two Fiddle::Pointer.malloc(...,
269
+ # RUBY_FREE) calls per cast — each registering a GC finalizer — measured as the
270
+ # dominant per-call cost by an order of magnitude. The NumFormat no longer lives here:
271
+ # each format owns its own packed pointer (see packed_cache), so a numeric call copies
272
+ # nothing into scratch before crossing.
268
273
  def scratch
269
274
  Thread.current[:hypercast_scratch] ||= begin
270
- base = Fiddle::Pointer.malloc(36, Fiddle::RUBY_FREE)
271
- [base, base + 16, base + 24]
275
+ base = Fiddle::Pointer.malloc(24, Fiddle::RUBY_FREE)
276
+ [base, base + 16]
272
277
  end
273
278
  end
274
279
 
@@ -288,24 +293,31 @@ module HyperCast
288
293
  # The shared body of every format-free door: one native call over the scratch buffers.
289
294
  def plain(symbol, text, out_size)
290
295
  bytes = utf8(text)
291
- out, fault, = scratch
292
- rc = Runtime.call(symbol, input_ptr(bytes), bytes.bytesize, out, fault)
296
+ out, fault = scratch
297
+ rc = Runtime.function(symbol).call(input_ptr(bytes), bytes.bytesize, out, fault)
293
298
  verdict(rc, fault) { yield(out[0, out_size]) }
294
299
  end
295
300
 
296
- # The shared body of the integer/real doors: plain, plus the packed NumFormat.
301
+ # The shared body of the integer/real doors: plain, plus the format's own packed
302
+ # pointer, passed straight through — no per-call copy of the 12 bytes into scratch.
297
303
  def numeric(symbol, text, format, out_size)
298
304
  bytes = utf8(text)
299
- out, fault, raw_format = scratch
300
- raw_format[0, 12] = packed_cache[format]
301
- rc = Runtime.call(symbol, input_ptr(bytes), bytes.bytesize, raw_format, out, fault)
305
+ out, fault = scratch
306
+ rc = Runtime.function(symbol).call(input_ptr(bytes), bytes.bytesize, packed_cache[format], out, fault)
302
307
  verdict(rc, fault) { yield(out[0, out_size]) }
303
308
  end
304
309
 
305
- # Identity-keyed memo of NumFormat#packed formats are reused constants in practice,
306
- # and re-packing per call is a measurable allocation. The race is benign (idempotent).
310
+ # Identity-keyed memo (compare_by_identitya pointer hash, not Data's structural
311
+ # #hash over two Strings and an Integer) of a native 12-byte RawNumFormat per format
312
+ # object, filled once from NumFormat#packed. Formats are reused constants in practice,
313
+ # so the common call finds its pointer in one lookup and packs nothing. The Hash holds
314
+ # the format, so a key can never be a recycled address; the race is benign (idempotent).
307
315
  def packed_cache
308
- @packed_cache ||= Hash.new { |cache, format| cache[format] = format.packed }
316
+ @packed_cache ||= Hash.new do |cache, format|
317
+ pointer = Fiddle::Pointer.malloc(12, Fiddle::RUBY_FREE)
318
+ pointer[0, 12] = format.packed
319
+ cache[format] = pointer
320
+ end.compare_by_identity
309
321
  end
310
322
 
311
323
  # Builds a UTC Time from the core's protobuf-shaped {seconds, nanos} pair, exactly.
@@ -325,10 +337,24 @@ HyperCast::BACKEND =
325
337
  if ENV["HYPERCAST_PURE"]
326
338
  :fiddle
327
339
  else
340
+ # Two layouts, and both have to work. A released platform gem is a "fat" gem carrying one
341
+ # extension per supported Ruby ABI under lib/hypercast/<minor>/ (see the Rakefile's
342
+ # native:gem task for why an ABI-per-file is unavoidable — Magnus has no `abi3`
343
+ # equivalent). CI's in-job staging and a local `cargo build --release --features ruby`
344
+ # instead drop a single extension flat at lib/. Trying the versioned path first and the
345
+ # flat one second means neither has to know the other exists.
346
+ #
347
+ # A miss on both is not an error: it means this Ruby/platform combination has no
348
+ # precompiled extension, which is precisely what the Fiddle backend is for.
328
349
  begin
329
- require "hypercast_native"
350
+ require "hypercast/#{RUBY_VERSION[/\d+\.\d+/]}/hypercast_native"
330
351
  :native
331
352
  rescue LoadError
332
- :fiddle
353
+ begin
354
+ require "hypercast_native"
355
+ :native
356
+ rescue LoadError
357
+ :fiddle
358
+ end
333
359
  end
334
360
  end
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.1.0
4
+ version: 0.2.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Brian Buvinghausen
@@ -64,6 +64,8 @@ files:
64
64
  - LICENSE
65
65
  - README.md
66
66
  - lib/hypercast.rb
67
+ - lib/hypercast/3.4/hypercast_native.so
68
+ - lib/hypercast/4.0/hypercast_native.so
67
69
  - lib/hypercast/native/linux-arm64/libhypercast.so
68
70
  - lib/hypercast/native/linux-x64/libhypercast.so
69
71
  - lib/hypercast/native/osx-arm64/libhypercast.dylib
@@ -72,7 +74,6 @@ files:
72
74
  - lib/hypercast/native/win-x64/hypercast.dll
73
75
  - lib/hypercast/native_platform.rb
74
76
  - lib/hypercast/runtime.rb
75
- - lib/hypercast_native.so
76
77
  homepage: https://github.com/SkunkWerkx/HyperCast
77
78
  licenses:
78
79
  - MIT
@@ -85,14 +86,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
85
86
  requirements:
86
87
  - - ">="
87
88
  - !ruby/object:Gem::Version
88
- version: '3.2'
89
+ version: '3.4'
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '4.1'
89
93
  required_rubygems_version: !ruby/object:Gem::Requirement
90
94
  requirements:
91
95
  - - ">="
92
96
  - !ruby/object:Gem::Version
93
97
  version: '0'
94
98
  requirements: []
95
- rubygems_version: 3.6.9
99
+ rubygems_version: 4.0.16
96
100
  specification_version: 4
97
101
  summary: Allocation-free scalar parsing as Success/Fault verdicts over a native Rust
98
102
  core
Binary file