Repository navigation
Description
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 cancellationruby2_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