Class: Bundler::Spinel::Verifier
- Inherits:
-
Object
- Object
- Bundler::Spinel::Verifier
- Defined in:
- lib/bundler/spinel/verifier.rb
Overview
The ‘verified` rung: differential testing. Runs a smoke program that exercises the gem once under CRuby and once compiled by Spinel, and compares stdout. This is the only signal that catches Spinel’s silent miscompiles (local-var-name collapse, Int-0-as-nil) — they emit no warning and exit 0, so the cheap probe can’t see them, but a differential run does: CRuby and the miscompiled binary diverge.
match, behaviour smoke -> verified (CRuby and Spinel agree on real use)
match, require-only -> loaded (loads+runs identically; logic untested)
mismatch -> rejected (reason: miscompile, with a short diff)
no build -> rejected (reason: build-error / run-error)
The smoke is the unit of trust. The require-only default smoke only proves the gem loads identically (‘loaded`); pass `–smoke FILE` (a snippet that drives the gem’s API and prints deterministic output) to actually verify behaviour (‘verified`). Verification is only as good as the smoke — which is why it’s opt-in and human-supplied. (A ‘loaded` gem can still silently miscompile in logic the require-only smoke never ran — observed in practice.)
Constant Summary collapse
- HARNESS =
"__spinel_verify.rb".freeze
Instance Method Summary collapse
-
#initialize(engine, ledger) ⇒ Verifier
constructor
A new instance of Verifier.
-
#verify(gem_name, version, dir, smoke: nil, full: false, rbs: :auto) ⇒ Object
full: when true, the harness force-requires every .rb under the gem’s lib/ (not just the entrypoint) before the smoke body.
Constructor Details
#initialize(engine, ledger) ⇒ Verifier
Returns a new instance of Verifier.
27 28 29 30 |
# File 'lib/bundler/spinel/verifier.rb', line 27 def initialize(engine, ledger) @engine = engine @ledger = ledger end |
Instance Method Details
#verify(gem_name, version, dir, smoke: nil, full: false, rbs: :auto) ⇒ Object
full: when true, the harness force-requires every .rb under the gem’s lib/ (not just the entrypoint) before the smoke body. This defeats the ‘autoload`/lazy-`require` masking that lets a constant-only smoke pass `verified` while the gem’s real surface (client/transport/serialization) never compiled — the qdrant-ruby spike (spinelgems#4). The verdict vocabulary is unchanged; the probe is tagged ‘verify-full` so the whole-surface signal stays distinguishable in the ledger from the entrypoint-only `verify`. rbs: a gem’s shipped sig/*.rbs acts as the type root for the Spinel compile (spinelgems#13). ‘–rbs` re-pins uncalled public methods’ param/return/ivar types that whole-program inference would otherwise widen to int/poly for lack of a call site — the failure mode hand-written seed blocks exist to patch. :auto (default) uses <dir>/sig when it contains .rbs files; a String is an explicit root; nil/false disables.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/bundler/spinel/verifier.rb', line 46 def verify(gem_name, version, dir, smoke: nil, full: false, rbs: :auto) @engine.ensure! rbs_root = resolve_rbs_root(dir, rbs) harness = File.join(dir, HARNESS) File.write(harness, harness_source(gem_name, dir, smoke, full)) ruby_out, ruby_err, ruby_ok = run_ruby(harness, dir) spin_out, spin_err, spin_ok = run_spinel(harness, rbs_root) verdict, reasons = classify(ruby_ok, spin_ok, ruby_out, spin_out, spin_err, behavior: !smoke.nil?) # Tag *why* (the spinelgems#4 usability rubric) so a failure says what it'd # take, not just "rejected". Prepended to reasons as `rubric:<tag>`. unless verdict == "verified" || verdict == "loaded" || verdict == "clean" reasons = ["rubric:#{rubric(ruby_ok, ruby_err, spin_ok, spin_err, ruby_out, spin_out, gem_name)}"] + reasons end # Self-localize a miscompile: a `diff:L2 cruby=… spinel=…` reason says # the outputs differ but not where the value went wrong. The bisector # traces the still-on-disk harness (require_relative'd gem files included) # and, when it pins a diverging scalar, appends `localized:<file>:<line> # <var> cruby=… spinel=…`. Best-effort: nil when it can't localize. if verdict == "rejected" && reasons.include?("miscompile") # Note: the bisector compiles without the sig root, so localization of # an rbs-seeded build is best-effort (bisect.sh has no --rbs passthrough). loc = Localizer.new(@engine).localize(harness) reasons += [loc] if loc end # Provenance: the verdict was reached with the gem's sig/ as type root — # legible in the ledger/notes, ignored by the site's sticky logic. reasons += ["rbs:sig"] if rbs_root @ledger.record(@ledger.build( gem: gem_name, version: version, rev: @engine.rev, verdict: verdict, reasons: reasons, probe: full ? "verify-full" : "verify" )) ensure File.delete(harness) if harness && File.exist?(harness) end |