Skip to content

A native method cannot tell a positional Hash from keyword arguments, and raise's cause: pays for it #873

Open

Description

@tannevaled

A native method cannot tell a positional Hash from keyword arguments. It
receives args []object.Value with a trailing *object.Hash either way, and
object.Hash carries no marker, so these two calls are indistinguishable inside
a native:

raise("message", {cause: RuntimeError.new})   # TWO POSITIONAL arguments
raise("message", cause: RuntimeError.new)     # one argument and a keyword

MRI answers differently: the first is a message plus a second positional
argument that is not an exception class, so it is
TypeError: exception class/object expected; the second raises a RuntimeError
with that cause.

Measured, on a 4.0.9 build

$ rbgo probe.rb
Kernel#raise positional Hash: [RuntimeError, "message"]
Fiber#raise positional Hash:  [RuntimeError, "message"]

Both should be [TypeError, "exception class/object expected"].

popCauseKwarg (internal/vm/exception.go:345) pops any trailing one-entry
{cause: …} Hash, which is the only thing it can do with what it is given.

What it costs in ruby/spec

shared/kernel/raise.rb:93 — "raises a TypeError when passed a message and an
extra argument"
— is written with a brace literal:

-> { @object.raise("message", {cause: RuntimeError.new()}) }
    .should.raise(TypeError, "exception class/object expected")

so it fails wherever rbgo's cause handling runs. That is core/kernel/raise_spec.rb,
core/fiber/raise_spec.rb, and — since #872 gave Thread#raise the same
cause: support — core/thread/raise_spec.rb as well, twice over there
because the shared block is included twice.

⛔ Before #872, Thread#raise passed that example, and it passed because
of
a defect: it ignored cause: entirely, so there was no trailing Hash to
mispop. Trading it for the 17 examples the cause: keyword actually fixes is
the right trade, but it should be recorded as a trade and not as a clean win.

Scope

Fixing it means marking keyword arguments at the call boundary so a native can
ask — a change to the call path and to every native that inspects a trailing
Hash, not something a cause-handling patch can do locally. Filing rather than
fixing in place, since the defect is older than the change that exposed it and
wider than raise.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions