Issue #22111 has been reported by yertto (_ yertto). ---------------------------------------- Bug #22111: Non-symbolic hash keys with `expr : value` syntax https://bugs.ruby-lang.org/issues/22111 * Author: yertto (_ yertto) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- # Non-symbolic hash keys with `expr : value` syntax Allow `expr : value` as a computed hash key. 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; { name: "Alice", role: "admin", "key-#{n}": "foo", RUBY_VERSION:, n => "bar" }.keys # => [:name, :role, :"key-42", :RUBY_VERSION, 42] ## After -- uniform colon syntax n = 42; { name: "Alice", role: "admin", "key-#{n}": "foo", RUBY_VERSION:, n : "bar" }.keys # => [:name, :role, :"key-42", :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 | Symbol (quoted label) | | `{ value_omission: }` | v3.1 | Feature #14579 | Value omission | | `{ expr : value }` | *???* | **Feature #XXXXX** | Non-symbolic | ### Readability for computed-key-heavy code Code that builds dynamic hashes currently forces a style break midway through a literal. This is especially noticeable with computed keys, numeric keys, or variable-driven keys. ### 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 label-eligible expression (bareword identifier or quoted string) followed by `:` with **no space** 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 be labels never need a space, so there is no ambiguity to resolve: ```ruby { "a": 1 } # => {:a=>1} symbol key { "a" : 1 } # => {"a"=>1} string "a" { 42: 1 } # => {42=>1} integer 42 { Math::Pi: 1 } # => {3.141592653589793 => 1} ``` Other expressions also work without a space — there is no ambiguity to resolve: ```ruby { foo(): 1 } # computed, method call result { -42: 1 } # computed, negative integer { 1 ? 10 : 20: 1 } # computed, ternary expression result (1 is truthy) ``` ## 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` | The whitespace version is strictly more general, has a simpler implementation, and requires no lexer changes. ## Edge cases ```ruby { a: 1 } # => {:a=>1} symbol key (unchanged) { a : 1 } # => {42=>1} computed key { 42: 1 } # => {42=>1} computed key { -42: 1 } # => {-42=>1} computed key { "a" : 1 } # => {"a"=>1} computed key {%"a": 1} # => {"a"=>1} percent string as computed key { (1+2): "three" } # => {3=>"three"} parenthesized expression { n : n } # => {42=>42} expression as both key and value { n : } # syntax error value omission not supported { n : 1, } # => {42=>1} trailing comma ok { 1 ? 10 : 20: 1 } # => {10=>1} ternary inner, colon outer (1 is truthy) ``` ## 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. PR: [#17318](https://github.com/ruby/ruby/pull/17318) ## 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 - Should `{ a : }` be a value omission? (Would need compile-time inference.) - Is there community appetite for eventually deprecating `=>` from hash literals? ## 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. -- https://bugs.ruby-lang.org/