Remove hash map literals

In order to simplify the syntax, hash map literals have been removed.
Instead, one now creates a hash map using `Map.new`. To make it easier
to create a map with a bunch of pairs in one expression, we introduce
the `Map.set` method. This method behaves similar to `Map.[]=`, but
returns the Map itself instead of the value written:

    Map
      .new
      .set('foo', 'bar')
      .set('baz', 'quix')

So why were map literals removed? Well, first of all their syntax was
not intuitive: `%[key: value]`. This syntax was taken from Elixir, but
is not used in other languages that we are aware of. Many languages use
curly braces (e.g. `{key => value}`), but these are already used for
closures. Some languages reuse square brackets (e.g. `[key: value]`),
but this makes the meaning of `[]` unclear. We considered using a syntax
similar to Scala:

    Map.new(key -> value)

Here `->` would be a method that returns some sort of tuple, and
`Map.new` would take this list of tuples and use them to fill the map.
The method `->` would have to be available for every object, since
it's perfectly valid to use outside of constructing maps. This means
`->` would have to be defined on `Object`, or in a trait that is
implemented for it. Manually implementing the method/trait would be too
cumbersome. The hypothetical code for this might look as follows:

    impl Object {
      def ->!(V)(other: V) -> Tuple!(Self, V) {
        Tuple.new(self, other)
      }
    }

Unfortunately, the Ruby compiler does not support the use of self types
in generic types well enough to make this work. This is a long standing
issue [1], but it would require an extensive rewrite of the type system
to support. Since we want to rewrite the Ruby compiler in Inko, adding
support for this in the Ruby compiler would be a waste of time.

There are a variety of other approaches, such as passing a closure to
`Map.new` that can be used to fill up the map. All of these suffer from
similar problems: the Ruby compiler's type system is a bit buggy.

To work around all of this, we added the `Map.set` method. While the
resulting code is a bit more verbose, it does not require any compiler
changes. The API should also feel familiar to those used to immutable
programming languages, which typically use a similar approach for
constructing hash maps.

The removal of map literals also allows us to remove various compiler
optimisations of these literals, simplifying the compiler and making the
language more predictable.

[1]: #107
parent 7fe9e24f
Pipeline #75838437 passed with stages
in 32 minutes and 14 seconds
......@@ -99,10 +99,6 @@ module Inkoc
false
end
def hash_map_literal?
false
end
def global?
false
end
......
......@@ -49,14 +49,6 @@ module Inkoc
name == Config::NEW_MESSAGE
end
def hash_map_literal?
receiver&.global? &&
receiver&.name == Config::HASH_MAP_LITERAL_RECEIVER &&
name == Config::FROM_ARRAY_MESSAGE &&
arguments[0]&.array_literal? &&
arguments[1]&.array_literal?
end
def raw_instruction_visitor_method
:"on_raw_#{name}"
end
......
......@@ -32,7 +32,6 @@ module Inkoc
TRAIT_MODULE = 'trait'
INTERNAL_TRAIT_IMPORT = '_inkoc_std_trait'
HASH_MAP_LITERAL_RECEIVER = '_inkoc_hash_map_literal'
MARKER_MODULE = 'std::marker'
......@@ -40,7 +39,6 @@ module Inkoc
OBJECT_CONST = 'Object'
TRAIT_CONST = 'Trait'
ARRAY_CONST = 'Array'
HASH_MAP_CONST = 'Map'
BLOCK_CONST = 'Block'
INTEGER_CONST = 'Integer'
FLOAT_CONST = 'Float'
......@@ -66,7 +64,6 @@ module Inkoc
UNKNOWN_MESSAGE_MESSAGE = 'unknown_message'
UNKNOWN_MESSAGE_TRAIT = 'UnknownMessage'
UNKNOWN_MESSAGE_MODULE = 'std::unknown_message'
FROM_ARRAY_MESSAGE = 'from_array'
SET_INDEX_MESSAGE = '[]='
MODULE_GLOBAL = 'ThisModule'
CALL_MESSAGE = 'call'
......
......@@ -106,7 +106,7 @@ module Inkoc
when '"' then return double_string
when ':' then return colons
when '/' then return div
when '%' then return modulo_or_hash_open
when '%' then return modulo
when '^' then return bitwise_xor
when '&' then return bitwise_and_or_boolean_and
when '|' then return bitwise_or_or_boolean_or
......@@ -436,20 +436,8 @@ module Inkoc
new_token(token_type, start, @position)
end
def modulo_or_hash_open
start = @position
token_type = :mod
case @input[@position += 1]
when '['
token_type = :hash_open
@position += 1
when '='
token_type = :mod_assign
@position += 1
end
new_token(token_type, start, @position)
def modulo
operator(1, :mod, :mod_assign)
end
def bitwise_xor
......
......@@ -59,7 +59,6 @@ module Inkoc
define
do
float
hash_open
identifier
impl
integer
......@@ -583,7 +582,6 @@ module Inkoc
when :constant then constant(start)
when :curly_open then block_without_arguments(start)
when :bracket_open then array(start)
when :hash_open then hash(start)
when :define then def_method(start)
when :static then def_static_method(start)
when :do, :lambda then block(start, start.type)
......@@ -764,42 +762,6 @@ module Inkoc
new_array(values, start)
end
# Parses a hash map literal
#
# Example:
#
# %['key': 'value']
def hash(start)
keys = []
vals = []
while (key_tok = @lexer.advance) && key_tok.valid_but_not?(:bracket_close)
key = expression(key_tok)
advance_and_expect!(:colon)
value = expression(advance!)
keys << key
vals << value
break if comma_or_break_on(:bracket_close)
end
location = start.location
receiver = AST::Global.new(Config::HASH_MAP_LITERAL_RECEIVER, location)
keys_array = new_array(keys, start)
vals_array = new_array(vals, start)
AST::Send.new(
Config::FROM_ARRAY_MESSAGE,
receiver,
[],
[keys_array, vals_array],
location
)
end
# Parses a method definition.
#
# Examples:
......
......@@ -461,8 +461,6 @@ module Inkoc
def receiver_type_for_send_with_receiver(node, scope)
if node.name == Config::NEW_MESSAGE
define_type_instance(node.receiver, scope)
elsif node.hash_map_literal?
@module.lookup_global(Config::HASH_MAP_CONST)
else
define_type(node.receiver, scope)
end
......
......@@ -522,12 +522,6 @@ module Inkoc
end
def on_send(node, body)
# Map literals need to be optimised before we process their
# arguments.
if node.hash_map_literal?
return on_hash_map_literal(node, body)
end
receiver = receiver_for_send(node, body)
args, kwargs = split_send_arguments(node.arguments, body)
......@@ -543,64 +537,6 @@ module Inkoc
)
end
# Optimises a Map literal.
#
# This method will turn this:
#
# let x = %['a': 10, 'b': 20]
#
# Into (effectively) the following:
#
# let hash_map = Map.new
#
# hash_map['a'] = 10
# hash_map['b'] = 20
#
# let x = hash_map
#
# While the example above uses a local variable `hash_map`, the generated
# code only uses registers.
def on_hash_map_literal(node, body)
hash_map_global_reg =
get_global(Config::HASH_MAP_CONST, body, node.location)
hash_map_type = hash_map_global_reg.type
new_method = hash_map_type.lookup_method(Config::NEW_MESSAGE).type
set_method = hash_map_type.lookup_method(Config::SET_INDEX_MESSAGE).type
# Initialise an empty Map.
hash_map_reg = send_object_message(
hash_map_global_reg,
new_method.name,
[],
[],
new_method,
node.type,
body,
node.location
)
keys = node.arguments[0].arguments
vals = node.arguments[1].arguments
# Every key-value pair is compiled into a `hash[key] = value`
# expression.
keys.zip(vals).each do |(knode, vnode)|
send_object_message(
hash_map_reg,
set_method.name,
[process_node(knode, body), process_node(vnode, body)],
[],
set_method,
vnode.type,
body,
knode.location
)
end
hash_map_reg
end
def split_send_arguments(arguments, body)
args = []
kwargs = []
......
......@@ -445,26 +445,18 @@ describe Inkoc::Lexer do
end
end
describe '#modulo_or_hash_open' do
describe '#modulo' do
it 'tokenizes the modulo operator' do
lexer = described_class.new('%')
token = lexer.modulo_or_hash_open
token = lexer.modulo
expect(token.type).to eq(:mod)
expect(token.value).to eq('%')
end
it 'tokenizes the hash-open token' do
lexer = described_class.new('%[')
token = lexer.modulo_or_hash_open
expect(token.type).to eq(:hash_open)
expect(token.value).to eq('%[')
end
it 'tokenizes the module-assign operator' do
lexer = described_class.new('%=')
token = lexer.modulo_or_hash_open
token = lexer.modulo
expect(token.type).to eq(:mod_assign)
expect(token.value).to eq('%=')
......
......@@ -38,6 +38,7 @@ _INKOC.set_object_name(Block, 'Block')
# building blocks of Inko, such as "Object.new" and the bits necessary to allow
# creating of modules.
impl Object {
## Creates a new instance of `self` and sends `init` to the instance.
static def new -> Self {
let obj = _INKOC.set_object(False, self)
......
......@@ -61,18 +61,20 @@ let CURLY_CLOSE = 125
## The escape sequence literals supported by a single quoted string, and their
## replacement bytes.
let SINGLE_QUOTED_STRING_ESCAPE_SEQUENCES = %[SINGLE_QUOTE: SINGLE_QUOTE]
let SINGLE_QUOTED_STRING_ESCAPE_SEQUENCES =
Map.new.set(SINGLE_QUOTE, SINGLE_QUOTE)
## The escape sequence literals supported by a double quoted string, and their
## replacement bytes.
let DOUBLE_QUOTED_STRING_ESCAPE_SEQUENCES = %[
DOUBLE_QUOTE: DOUBLE_QUOTE,
LOWER_N: NEWLINE,
LOWER_T: TAB,
ZERO: NULL,
LOWER_E: ESCAPE,
LOWER_R: CARRIAGE_RETURN
]
let DOUBLE_QUOTED_STRING_ESCAPE_SEQUENCES =
Map
.new
.set(DOUBLE_QUOTE, DOUBLE_QUOTE)
.set(LOWER_N, NEWLINE)
.set(LOWER_T, TAB)
.set(ZERO, NULL)
.set(LOWER_E, ESCAPE)
.set(LOWER_R, CARRIAGE_RETURN)
## A `Lexer` is used for turning Inko source code into a sequence of tokens.
## These tokens in turn can be used by a parser to produce an Abstract Syntax
......@@ -590,22 +592,9 @@ object Lexer {
}
def percent -> Token {
next_byte == BRACKET_OPEN
.if_true {
return hash_open
}
operator(type: 'mod', assign_type: 'mod_assign')
}
def hash_open -> Token {
let start = @position
@position += 2
token(type: 'hash_open', start: start, line: @line)
}
def minus -> Token {
let next = next_byte
......
......@@ -68,10 +68,10 @@ def remove(variable: String) -> Nil {
##
## import std::env
##
## env.variables # => %[ 'HOME': '/home/alice', ... ]
## env.variables # => Map { 'HOME': '/home/alice', ... }
def variables -> Map!(String, String) {
let names = _INKOC.env_variables
let map = %[]
let map = Map.new
names.each do (name) {
let value = ThisModule[name]
......
......@@ -419,7 +419,7 @@ object LayoutBuilder {
def init {
@members = []
@types = []
@existing = %[]
@existing = Map.new
@alignment = 0
@padding = True
}
......@@ -441,7 +441,7 @@ object LayoutBuilder {
## Creates a `Layout` that automatically applies padding.
def layout_with_padding -> Layout {
let members = %[]
let members = Map.new
let mut size = 0
let mut offset = 0
let mut remaining_in_hole = @alignment
......@@ -479,7 +479,7 @@ object LayoutBuilder {
## Creates a `Layout` that does not use any padding.
def layout_without_padding -> Layout {
let members = %[]
let members = Map.new
let mut offset = 0
@members.each_with_index do (name, index) {
......
......@@ -186,9 +186,12 @@ impl Inspect for Map!(K, V) {
##
## Inspecting a `Map`:
##
## let map = %['name': 'Alice', 'address': 'Foo Street']
## let map = Map.new
##
## map.inspect # => '%["name": "Alice", "address": "Foo Street"]'
## map['name'] = 'Alice'
## map['address'] = 'Foo Street'
##
## map.inspect # => 'Map { "name": "Alice", "address": "Foo Street" }'
def inspect -> String where K: Inspect, V: Inspect {
::format.inspect(self)
}
......@@ -198,7 +201,13 @@ impl Inspect for Map!(K, V) {
let last = length - 1
let mut index = 0
formatter.push('%[')
formatter.push('Map')
empty?.if_true {
return
}
formatter.push(' { ')
each do (key, value) {
formatter.descend {
......@@ -219,6 +228,6 @@ impl Inspect for Map!(K, V) {
index += 1
}
formatter.push(']')
formatter.push(' }')
}
}
......@@ -80,7 +80,7 @@ object RandomState {
}
}
## A single key-value pair
## A key-value pair with a pre-computed hash.
object Pair!(K: Hash + Equal, V) {
## The key that was hashed.
@key: K
......@@ -172,29 +172,11 @@ object Map!(K: Hash + Equal, V) {
## Returns a `Map` using two arrays: one containing the keys and one
## containing the values.
##
## Using this method is semantically equivalent to creating a `Map` using
## `Map.new` and sending `[]=` to the `Map` for every key-value pair.
## In other words, this:
##
## %['name': 'Alice']
##
## Is the same as this:
##
## let mut map = Map.new
##
## map['name'] = 'Alice'
##
## # Compiler optimisation
##
## To remove the need for allocating two arrays for `Map` literals, the
## compiler may decide to optimise this method into separate `[]=` message
## sends as illustrated above.
##
## # Examples
##
## Creating a `Map` from two arrays:
##
## Map.from_array(['name'], ['Alice']) # => %['name': 'Alice']
## Map.from_array(['name'], ['Alice'])
static def from_array!(K: Hash + Equal, V)(
keys: Array!(K),
values: Array!(V)
......@@ -219,7 +201,6 @@ object Map!(K: Hash + Equal, V) {
map
}
## Creates a new, empty `Map`.
def init {
@random_state = RandomState.new
@buckets = []
......@@ -368,7 +349,9 @@ object Map!(K: Hash + Equal, V) {
##
## Checking if a `Map` defines a key:
##
## let map = %['name': 'Alice']
## let map = Map.new
##
## map['name'] = 'Alice'
##
## map.key?('name') # => True
## map.key?('city') # => False
......@@ -380,6 +363,27 @@ object Map!(K: Hash + Equal, V) {
}
}
## Inserts the key and value in this `Map`, returning the `Map` itself.
##
## This method makes it possible to create a `Map` and store many key-value
## pairs, all in a single message chain.
##
## # Examples
##
## Inserting multiple key-value pairs:
##
## let map = Map.new.set('a', 10).set('b', 20)
def set(key: K, value: V) -> Self {
@length >= @resize_threshold
.if_true {
rehash
}
_insert_pair(Pair.new(key: key, value: value, hash: _hash_key(key)))
self
}
## Resizes and rehashes `self`.
def rehash {
let old_buckets = @buckets
......@@ -529,7 +533,13 @@ impl Equal for Map!(K, V) {
##
## Comparing two `Map` instances:
##
## %w['name': 'Alice'] == %w['name': 'Alice'] # => True
## let map1 = Map.new
## let map2 = Map.new
##
## map1['name'] = 'Alice'
## map2['name'] = 'Alice'
##
## map1 == map2 # => True
def ==(other: Self) -> Boolean where V: Equal {
length == other.length
.if_false {
......@@ -581,8 +591,7 @@ impl Index!(K, V) for Map!(K, V) {
}
impl SetIndex!(K, V) for Map!(K, V) {
## Inserts the given key and value into this map, returning the inserted
## value.
## Inserts the key and value in this `Map`, returning the inserted value.
##
## # Examples
##
......@@ -592,13 +601,7 @@ impl SetIndex!(K, V) for Map!(K, V) {
##
## map['name'] = 'Alice' # => 'Alice'
def []=(key: K, value: V) -> V {
@length >= @resize_threshold
.if_true {
rehash
}
_insert_pair(Pair.new(key: key, value: value, hash: _hash_key(key)))
set(key: key, value: value)
value
}
}
......
......@@ -592,16 +592,6 @@ world\n" "hello"'
assert.equal(token.location.line_range, 1..1)
}
g.test('Lexing the hash open operator') {
let token = lex('%[')
assert.equal(token.type, 'hash_open')
assert.equal(token.value, '%[')
assert.equal(token.location.column, 1)
assert.equal(token.location.line_range, 1..1)
}
g.test('Lexing the XOR operator') {
let token = lex('^')
......
......@@ -428,7 +428,7 @@ test.group('std::ffi::Member.offset') do (g) {
test.group('std::ffi::Layout.alignment') do (g) {
g.test('Obtaining the alignment of a Layout') {
let layout = Layout.new(members: %[], alignment: 8, size: 4)
let layout = Layout.new(members: Map.new, alignment: 8, size: 4)
assert.equal(layout.alignment, 8)
}
......@@ -436,7 +436,7 @@ test.group('std::ffi::Layout.alignment') do (g) {
test.group('std::ffi::Layout.size') do (g) {
g.test('Obtaining the size of a Layout') {
let layout = Layout.new(members: %[], alignment: 8, size: 4)
let layout = Layout.new(members: Map.new, alignment: 8, size: 4)
assert.equal(layout.size, 4)
}
......@@ -445,14 +445,18 @@ test.group('std::ffi::Layout.size') do (g) {
test.group('std::ffi::Layout.[]') do (g) {
g.test('Obtaining a Member using a valid name') {
let member = Member.new(name: 'tm_sec', type: types.i32, offset: 0)
let layout = Layout.new(members: %['tm_sec': member], alignment: 8, size: 4)
let members = Map.new
members['tm_sec'] = member
let layout = Layout.new(members: members, alignment: 8, size: 4)
assert.equal(layout['tm_sec'], member)
}
g.test('Obtaining a Member using an invalid name') {
assert.panic {
let layout = Layout.new(members: %[], alignment: 8, size: 4)
let layout = Layout.new(members: Map.new, alignment: 8, size: 4)
layout['tm_sec']
}
......@@ -472,7 +476,7 @@ test.group('std::ffi::Layout.from_pointer') do (g) {
test.group('std::ffi::Struct.size') do (g) {
g.test('Obtaining the size of a Struct') {
let layout = Layout.new(members: %[], alignment: 8, size: 4)
let layout = Layout.new(members: Map.new, alignment: 8, size: 4)
let struct = Struct.new(pointer: Pointer.null, layout: layout)
assert.equal(struct.size, 4)
......@@ -481,7 +485,7 @@ test.group('std::ffi::Struct.size') do (g) {
test.group('std::ffi::Struct.alignment') do (g) {
g.test('Obtaining the alignment of a Struct') {
let layout = Layout.new(members: %[], alignment: 8, size: 4)
let layout = Layout.new(members: Map.new, alignment: 8, size: 4)
let struct = Struct.new(pointer: Pointer.null, layout: layout)
assert.equal(struct.alignment, 8)
......@@ -490,7 +494,7 @@ test.group('std::ffi::Struct.alignment') do (g) {
test.group('std::ffi::Struct.pointer') do (g) {
g.test('Obtaining the alignment of a Struct') {
let layout = Layout.new(members: %[], alignment: 8, size: 4)
let layout = Layout.new(members: Map.new, alignment: 8, size: 4)
let struct = Struct.new(pointer: Pointer.null, layout: layout)
assert.equal(struct.pointer, Pointer.null)
......
......@@ -130,10 +130,20 @@ test.group('std::array::Array.inspect') do (g) {
test.group('std::map::Map.inspect') do (g) {
g.test('Inspecting an empty Map') {
assert.equal(%[].inspect, '%[]')
assert.equal(Map.new.inspect, 'Map')
}
g.test('Inspecting a non-empty Map') {
assert.equal(%['key': 10].inspect, '%["key": 10]')
let map = Map.new
map['foo'] = 10
map['bar'] = 20