Feature #22111
Updated by yertto (_ yertto) 4 months ago
# Non-symbolic hash keys with `expr : value` syntax
Allow `expr : value` for non-symbolic hash keys.
Almost 20 years in the making, the missing puzzle piece for Hash's "new" colon syntax:
```ruby
h = {
name: "symbol shorthand", # Ruby 1.9+
"quoted label": "symbol label", # Ruby 2.2+ (Feature #4935)
value_omission: , # Ruby 3.1+ (Feature #14579)
expr : "non symbol", # THIS PROPOSAL
}
```
## Motivation
Ruby has several colon-based hash key syntaxes for symbolic keys, but the `=>` ("hash rocket") is still needed for non-symbolic keys.
Adding `expr : value` is a backwards compatible leap forward toward allowing all-colon hashes for all key types:
```ruby
## Before -- mixed styles
n = 42; { key1: "symbol", "key-#{2}": "quoted symbol", RUBY_VERSION:, n => "bar" }.keys
# => [:key1, :"key-2", :RUBY_VERSION, 42]
## After -- uniform colon syntax
n = 42; { key1: "symbol", "key-#{2}": "quoted symbol", RUBY_VERSION:, n : "bar" }.keys
# => [:key1, :"key-2", :RUBY_VERSION, 42]
```
Could this be what we need to one day retire our old friend "hash rocket" from Hashes entirely?
### Completing the colon family
| Example | Ruby | Feature | Key type |
|---------|------|---------|----------|
| `{ name: value }` | v1.9 | | Symbol |
| `{ "quoted label": value }` | v2.2 | [Feature #4935](https://bugs.ruby-lang.org/issues/4935) | Symbol (quoted label) |
| `{ value_omission: }` | v3.1 | [Feature #14579](https://bugs.ruby-lang.org/issues/14579) | Value omission |
| `{ expr : value }` | *???* | [Feature #22111](https://bugs.ruby-lang.org/issues/22111) | Non-symbolic |
### Reducing `=>` overloading
The `=>` token is now being used to serve more purposes than just the Hash literal (aka "Hash rocket") it was originally used for.
- rightward assignment
```
expr => var
```
- pattern capture
```
case {name: "Alice", role: "admin"}
in {name: String => name, role:} # capture the name String value into `name`
p name # => "Alice"
end
```
- rescue variables
```
rescue SomeExceptionClass => e
```
```
rescue => e
```
Reducing the rocket's use in Hashes simplifies the language, especially for newcomers.
## Design
### Disambiguation
A symbol eligible label-eligible expression (bareword identifier or quoted string) followed by `:` with **no space** becomes is a symbol key.
The same expression followed by `:` with **a space** cannot be a label, so it becomes a computed key:
```ruby
{ a: 1 } # => {:a => 1} symbol key, label syntax
{ a : 1 } # => {1 => 1} variable `a`, expr-colon (space disambiguates)
```
Expressions that cannot become symbols be labels never need a space, so there is no ambiguity to resolve:
```ruby
{ "a": 1 } # => {:a => 1}
{ "a" : 1 } # => {"a" => 1}
{ 42: 1 } # => {42 => 1}
{ Math::Pi: 1 } # => {3.141592653589793 => 1}
{ foo(): 1 } # => {"bar" => 1}
```
## Relationship to Feature #22108
The earlier proposal [Feature #22108](https://bugs.ruby-lang.org/issues/22108) suggested `{ (expr): value }` using parenthesized expressions with a lexer-generated label token (`tLABEL_END`).
I assumed there'd be too many dragons to fight with whitespace sensitivity.
However, after making the code changes somehow it just worked for all the test cases I threw at it. ¯\\_(ツ)_/¯
| | `(expr): value` | `expr : value` |
|---|---|---|
| Parser changes | Lexer + grammar | Grammar only |
| New fields | `hash_nest` | None |
| LALR conflicts | 0 | 0 |
| Parens required? | Yes | No |
| Interpolated key | `("key-#{n}"): val` | `"key-#{n}" : val` |
| Integer key | `(42): val` | `42: val` |
This version is strictly more general, has a simpler implementation, and requires no lexer changes.
## More examples
```ruby
key3 = "key3"
h = {
key1: 1,
"key-2": :two,
key3 : "3-expr",
"key4" : "4-String",
(5+0): "5-parentheses",
6: "6-Integer",
7.001: "7-Float",
"key" + "8": "8-String expr",
9+0: "9-Integer expr",
[10, 0]: "10-Array",
true ? 11 : 0 : "11-ternary"
}
p h
#=> {key1: 1, "key-2": :two, "key3" => "3-expr", "key4" => "4-String", 5 => "5-parentheses", 6 => "6-Integer", 7.001 => "7-Float", "key8" => "8-String expr", 9 => "9-Integer expr", [10, 0] => "10-Array", 11 => "11-ternary"}
p h.keys
#=> [:key1, :"key-2", "key3", "key4", 5, 6, 7.001, "key8", 9, [10, 0], 11]
```
### Familiar to developers from other languages
Python dicts have always allowed any hashable key types with `:` syntax.
With this proposal something like `{200: "OK", 404: "Not Found"}` can be used in either language.
## Edge cases
```ruby
{%"a": 1} # => {"a" => 1} percent string as computed key
{ n : } # syntax error value omission not supported
{ n : 1, } # => {42 => 1} trailing comma ok
```
## Implementation
Two files (plus tests):
- **`parse.y`**: One new production in `assoc`: `| arg_value ':' arg_value`
- **`prism/prism.c`**: In `parse_assocs`, accept `PM_TOKEN_COLON` when `pm_symbol_node_label_p` returns false
Zero lexer changes, zero new fields, zero LALR conflicts.
Reference implementation: [feature/expr-colon-hash-keys](https://github.com/ruby/ruby/pull/17318) PR on GitHub.
## Historical context
A version of this was discussed on ruby-core in October 2007 (as part of ["General hash keys for colon notation"](https://public-inbox.org/ruby-core/E1ImQBE-00033s-IZ@x31/T/), murphy).
Unfortunately it was brought up during the v1.9 feature freeze, but it looks like [Matz's invitation to discuss for v2.0](https://public-inbox.org/ruby-core/E1ImQA8-00033T-8c@x31/) didn't end up going anywhere.
Since then, `"quoted label":` (Ruby 2.2, Feature #4935) and value omission (Ruby 3.1, Feature #14579) have expanded the colon family, making the computed-key gap more conspicuous.
This proposal fills that gap with a minimal grammar change that requires no new lexer states.
## Open questions
- Is this syntax acceptable to the community?
- Should `{ a : }` be a value omission? (Would need compile-time inference.)
## Future directions
All colon-based key syntaxes would now be available.
This opens up the possibility of eventually deprecating `=>` from Hash literals (while keeping it for rescue, pattern matching, and rightward assignment).
This proposal does not require that change — it is simply the enabling step, and any deprecation timeline could be a separate discussion.