Skip to content

ast: HashLit cannot distinguish f({k: 1}) from f(k: 1) #40

Closed

Description

@tannevaled

f({k: 1}) and f(k: 1) produce the same AST, so nothing downstream can tell them apart

In Ruby 3+ these are different calls: the first passes a positional Hash, the second passes keywords. ast.HashLit records neither, so a consumer cannot distinguish them.

// ast/ast.go
type HashLit struct {
	Keys   []Node
	Values []Node
}

Two sites build one — parser.go:2376 for the braced literal and parser.go:3267 for the collected keyword pairs — and their output is structurally identical. ast.Call carries Recv, Name, Args, Block, Safe and no position, so a consumer cannot re-read the source to recover the brace either.

Measured against ruby 4.0.5

go-embedded-ruby/ruby at the current main, which is on parser v0.8.0:

def f(*a, **k) = [a, k]
p f({x: 1})   # MRI [[{x: 1}], {}]   rbgo [[], {x: 1}]   <- diverges
p f(x: 1)     # MRI [[], {x: 1}]     rbgo [[], {x: 1}]
h = {y: 2}
p f(h)        # MRI [[{y: 2}], {}]   rbgo [[{y: 2}], {}]  <- a VARIABLE is fine
p f(**h)      # MRI [[], {y: 2}]     rbgo [[], {y: 2}]

Only the braced literal at a call site diverges, which is exactly what the missing bit predicts: a variable holding a Hash is an ordinary argument node and is read correctly.

Why it is worth a field rather than a workaround

The divergence is usually invisible because a second error cancels it. Through a plain delegator the misreading at the call site is undone by the positional re-splat at the next hop:

def target(*a, **kw) = [a, kw]
def plain(*a) = target(*a)
p plain({k: 1})   # MRI [[{k: 1}], {}] | rbgo [[{k: 1}], {}]  -- agrees, by cancellation

ruby2_keywords removes the cancellation, because propagating the first reading faithfully is its whole job — so the underlying defect becomes visible as soon as a consumer gets keyword handling right. Downstream, this is the cause of one shape of core/module/ruby2_keywords_spec.rb and, by the consumer's count, 6 of the 9 remaining failures in language/keyword_arguments_spec.rb.

Suggested shape

A one-bit marker of the written form, set at parser.go:2376 and left false at parser.go:3267:

type HashLit struct {
	Keys   []Node
	Values []Node
	Braced bool // written as {…} at a call site: a POSITIONAL Hash, not keywords
}

There is precedent in the same file — RangeLit.Exclusive is exactly this: one bit recording how the literal was written, because the two forms mean different things. Adding a field with a false zero value keeps every existing consumer compiling and behaving as it does today; only a consumer that reads it changes.

Happy to send the PR, here and on the consumer side, if the shape is agreeable.

Activity

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