lyng/docs/proposals/map_literal.md
2025-11-28 11:25:47 +01:00

3.3 KiB

Map literals — refined proposal

Implement JavaScript-like literals for maps. The syntax and semantics align with named arguments in function calls, but map literals are expressions that construct Map values.

Keys can be either:

  • string literals: { "some key": value }, or
  • identifiers: { name: expr }, where the key becomes the string "name".

Identifier shorthand inside map literals is supported:

  • { name: } desugars to { "name": name }.

Property access sugar is not provided for maps: use bracket access only, e.g. m["a"], not m.a.

Examples:

val x = 2
val m = { "a": 1, x: x*10, y: }
assertEquals(1, m["a"])      // string-literal key
assertEquals(20, m["x"])     // identifier key
assertEquals(2, m["y"])      // identifier shorthand

Spreads (splats) in map literals are allowed and merged left-to-right with “rightmost wins” semantics:

val base = { a: 1, b: 2 }
val m = { a: 0, ...base, b: 3, c: 4 }
assertEquals(1, m["a"])  // base overwrites a:0
assertEquals(3, m["b"])  // literal overwrites spread
assertEquals(4, m["c"])  // new key

Trailing commas are allowed (optional):

val m = {
  "a": 1,
  b: 2,
  ...other,
}

Duplicate keys among literal entries (including identifier shorthand) are a compile-time error:

{ foo: 1, "foo": 2 }   // error: duplicate key "foo"
{ foo:, foo: 2 }        // error: duplicate key "foo"

Spreads are evaluated at runtime. Overlaps from spreads are resolved by last write wins. If a spread is not a map, or yields a map with non-string keys, it’s a runtime error.

Merging with +/+= and entries:

("1" => 10) + ("2" => 20)     // Map("1"=>10, "2"=>20)
{ "1": 10 } + ("2" => 20)     // same
{ "1": 10 } + { "2": 20 }    // same

var m = { "a": 1 }
m += ("b" => 2)                 // m = { "a":1, "b":2 }

Rightmost wins on duplicates consistently across spreads and merges. All map merging operations require string keys; encountering a non-string key during merge is a runtime error.

Empty map literal {} is not supported to avoid ambiguity with blocks/lambdas. Use Map() for an empty map.

Lambda disambiguation

  • A { ... } with typed lambda parameters must have a top-level -> in its header. The compiler disambiguates by looking for a top-level ->. If none is found, it attempts to parse a map literal; if that fails, it is parsed as a lambda or block.

Grammar (EBNF)

ws                    = zero or more whitespace (incl. newline/comments)
map_literal           = '{' ws map_entries ws '}'
map_entries           = map_entry ( ws ',' ws map_entry )* ( ws ',' )?
map_entry             = map_key ws ':' ws map_value_opt
                      | '...' ws expression
map_key               = string_literal | ID
map_value_opt         = expression | ε   // ε allowed only if map_key is ID

Notes:

  • Identifier shorthand (id:) is allowed only for identifiers, not string-literal keys.
  • Spreads accept any expression; at runtime it must yield a Map with string keys.
  • Duplicate keys are detected at compile time among literal keys; spreads are merged at runtime with last-wins.

Rationale

  • The { name: value } style is familiar and ergonomic.
  • Disambiguation with lambdas leverages the required -> in typed lambda headers.
  • Avoiding m.a sidesteps method/field shadowing and keeps semantics clear.