Class: Bundler::Spinel::Why

Inherits:
Object
  • Object
show all
Defined in:
lib/bundler/spinel/why.rb

Overview

‘spinel-compat why <gem>` — a legible “why doesn’t this gem work (yet)?” report (spinelgems#12). Turns a recorded (or freshly probed) Verdict into plain English: the cause, a category (Spinel limitation vs fixable compiler bug vs native C-ext vs metaprogramming vs dependency-blocked), the specific evidence, and what it would take — including whether the verdict is TERMINAL (won’t improve without an upstream/compiler change) or FIXABLE.

The data is already in the ledger (the rubric tag, the spinel warnings distilled into ‘reasons`, the static `risks`); this assembles it instead of making a user grep C output. Where deeper localization helps, it points at `why –probe` (live compiler output) / spinel-dev doctor for deeper localization.

Constant Summary collapse

EXPLAIN =

rubric/risk/reason signal -> structured explanation. :terminal is

:native    — won't work without a Spinel-native port (terminal here)
:limitation— a Spinel feature gap (improves when the compiler grows it)
:bug       — a fixable Spinel compiler bug (file/track upstream)
:dep       — blocked by a dependency (improves when the dep does)
:ok        — already usable; nothing to fix
{
  "c-extension" => {
    category: "native (C extension)", terminal: :native,
    cause: "ships a C extension; Spinel is whole-program AOT and never dlopens a .so.",
    take: "port the extension to Spinel's ffi_cflags/ffi_func DSL (tep/SpinelKit pattern), " \
          "or consume a pure-Ruby alternative. The CRuby .so cannot be vendored.",
  },
  "needs-dep" => {
    category: "dependency / not self-contained", terminal: :dep,
    cause: "fails under CRuby in the harness too — it needs an external gem, TLS, or network the probe doesn't provide.",
    take: "vendor the missing dependency (must itself be Spinel-compatible) or smoke only the offline surface.",
  },
  "load-path" => {
    category: "Spinel limitation (load path)", terminal: :limitation,
    cause: %(Spinel ignored a plain `require "gem/part"` (it has no load path), so the gem's real classes never compiled.),
    take: "restructure the gem to require_relative its own files (Spinel inlines those), or wait on load-path support.",
  },
  "needs-stdlib" => {
    category: "Spinel limitation (stdlib surface)", terminal: :limitation,
    cause: "requires a standard-library feature Spinel doesn't ship.",
    take: "use a Spinel-safe shim for that surface (SpinelKit consolidates these: JSON/Logger/…), or wait on stdlib coverage.",
  },
  "codegen" => {
    category: "compiler bug (codegen)", terminal: :bug,
    cause: "ordinary Ruby produced a C compile error — a fixable Spinel codegen bug, not a limitation of your code.",
    take: "file/track a matz/spinel issue. `why <gem> --probe` shows the live compiler error; spinel-dev doctor " \
          "localizes it to a file:line, and the harness usually has a minimal reproducer already.",
  },
  "miscompile" => {
    category: "compiler bug (silent miscompile)", terminal: :bug,
    cause: "it compiles and runs, but the output diverges from CRuby — the most dangerous failure, silently wrong.",
    take: "file a matz/spinel issue with the diff below; spinel-dev doctor + value-bisection localize it to a file:line + variable.",
  },
  "unsupported" => {
    category: "unsupported call (often metaprogramming)", terminal: :bug,
    cause: "Spinel could not resolve a call and silently emitted 0 — typically dynamic dispatch (send/define_method/extend).",
    take: "if it's a small codegen gap, file a matz/spinel issue; if it's deep metaprogramming, the surface is currently unsupported.",
  },
  "build-error" => {
    category: "build/run error", terminal: :bug,
    cause: "the Spinel build or run failed for a reason outside the other buckets.",
    take: "inspect the reasons below; `why <gem> --probe` re-runs the compiler and surfaces the raw error line.",
  },
  "smoke-error" => {
    category: "inconclusive (smoke broken under CRuby)", terminal: :dep,
    cause: "the behaviour smoke didn't run cleanly under plain CRuby, so no Spinel conclusion can be drawn.",
    take: "fix the smoke (a self-contained example of the gem's API), then re-verify.",
  },
  "analyze-oom" => {
    category: "compiler bug (analyzer OOM)", terminal: :bug,
    cause: "the Spinel analyzer exhausts memory on this gem (matz/spinel#1302); it's blacklisted from probing.",
    take: "terminal until matz/spinel#1302 lands; tracked there with reproducers.",
  },
  # Static pre-filter rejections (probe=static): a hard construct found by
  # source scan before any compile. Thread/Mutex compile now but misbehave
  # (matz/spinel#1360); TracePoint/ObjectSpace are out of the AOT model.
  "hard-construct" => {
    category: "Spinel limitation (runtime construct)", terminal: :limitation,
    cause: "uses a construct the static filter rejects before compiling (threads/mutexes/tracing).",
    take: "Thread/Mutex are single-thread-degradable (matz/spinel#1360); TracePoint/ObjectSpace/set_trace_func " \
          "are outside the closed-world AOT model. Often the gem works once that one construct is shimmed.",
  },
}.freeze
POSITIVE =
{
  "verified" => "compiles, and a behaviour smoke runs identically under CRuby and a Spinel-compiled binary. " \
                "Trustworthy — the only verdict that earns a curated-source slot.",
  "loaded"   => "compiles and loads identically to CRuby, but no behaviour smoke has exercised its logic at this rev — " \
                "so a silent miscompile in that logic is still possible. Run `verify --smoke <file>` to lift it to verified.",
  "clean"    => "compiles clean and uses no dynamic constructs Spinel degrades — but it has only been compiled, not run. " \
                "Run `verify` (require-only → loaded) or `verify --smoke` (behaviour → verified) to confirm it works.",
  "risky"    => "compiles, but the source uses dynamic constructs Spinel degrades silently — allowed by default, " \
                "rejected under `check --strict`. Whether it actually works depends on whether those paths run; " \
                "a behaviour smoke (`verify --smoke`) is the only way to know.",
}.freeze
USABLE =
%w[verified loaded clean risky].freeze

Instance Method Summary collapse

Constructor Details

#initialize(out: $stdout) ⇒ Why

Returns a new instance of Why.



101
102
103
# File 'lib/bundler/spinel/why.rb', line 101

def initialize(out: $stdout)
  @out = out
end

Instance Method Details

#report(v, source: "ledger") ⇒ Object

Render the report for a Verdict (from the ledger or a live probe).



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/bundler/spinel/why.rb', line 106

def report(v, source: "ledger")
  @out.puts
  @out.puts "spinel-compat why #{v.gem}  (#{v.version} @ #{v.rev || 'unknown rev'}, via #{source})"
  @out.puts

  glyph = { "verified" => "", "loaded" => "", "clean" => "", "risky" => "~", "rejected" => "" }[v.verdict] || "?"
  line "verdict", "#{glyph} #{v.verdict}"

  if USABLE.include?(v.verdict)
    positive(v, glyph)
  else
    negative(v)
  end
  @out.puts
  v
end