Issue with Maps and Dynamic Arrays?

I am having an issue, possibly misunderstanding something about Odin. I am putting a dynamic array of a distinct type. I try to determine if a key exists, and if it doesn’t, create a new dynamic array. Overwrite the lookup array value that was used to determine if the key existed, and append a value to it.

The problem is, for some reason that doesn’t work. Here is a minimal implementation:

package main

import "core:fmt"

Test :: distinct [2]int

main :: proc() {
	testMap := make(map[rune][dynamic]Test)

	lookup, has := &testMap['t']
	if !has {
		t := make([dynamic]Test)
		testMap['t'] = t
		lookup = &t
	}

	append(lookup, Test {0, 0})
	fmt.println(lookup, testMap)

	lookup2, has2 := &testMap['t']

	append(lookup2, Test {1, 1})
	fmt.println(lookup2, testMap)

	lookup3, has3 := &testMap['t']
	fmt.println(lookup3)
}

I get the following output:

&[[0, 0]] map[t=[]]
&[[1, 1]] map[t=[[1, 1]]]
&[[1, 1]]

I do not understand why the [0,0] entry is being dropped. Can anyone provide some insight? I thought the lookup = &t line would also update the same dynamic array that is now in the map, but that does not seem to be the case.

The reason the first append does not write into the map is because lookup is not pointing at a map entry.

The line causing the problem is this:

testMap['t'] = t

Here t is just copied into the map and any change to t does not affect the map.

You need to change the code to this:

lookup, has := &testMap['t']
if !has {
	testMap['t'] = make([dynamic]Test)
	lookup = &testMap['t']
}
1 Like

I guess I do not understand how dynamic arrays work in Odin then. I assumed that they would be a reference of some kind, but I am going to have to read more about them.

Thank you for your reply!

1 Like

Dynamic arrays and maps are both a small struct that contain a data pointer as well as some metadata.

https://pkg.odin-lang.org/base/runtime/#Raw_Dynamic_Array

Raw_Dynamic_Array :: struct {
	data:      rawptr, // effectively [^]T for [dynamic]T
	len:       int,
	cap:       int,
	allocator: Allocator,
}

https://pkg.odin-lang.org/base/runtime/#Raw_Map

Raw_Map :: struct {
	data:      uintptr,
	len:       uintptr,
	allocator: Allocator,
}

This is the reason why append takes a pointer, for instance–it needs to be able to modify the length to account for the newly-added value, and also possibly update the data pointer and capacity if it needs to re-allocate. This data is not behind the pointer.

As mentioned, when you do testMap['t'] = t, you are copying this struct (pointer + length + cap + allocator) into the map. You then set lookup to point to t–which is still the struct on the stack, not the one that got put into the map. The data at the pointer would not be cloned–both [dynamic]Tests would point at the same array in memory, if they had allocated, but have different metadata on it. But just make([dynamic]Test) doesn’t allocate, only set the allocator, so neither dynamic array has any backing memory yet.

When you then append to lookup, the copy of the struct that’s on the stack is allocated and updated, but the array in the map isn’t updated. Worse, if there were an allocation and append needed to re-allocate, the pointer in the map would still be pointing to the original, possibly-freed memory. In this case, both t and the dynamic array in the map have an allocator set but have no allocations, so you avoid any use-after-frees in this case.

In your case, this manifests in two ways:

  • When you print the map the first time, the length is still 0 in the [dynamic]Test in the map (since the copy in t was updated instead), and its data pointer is still nil and its capacity is 0. It is still actually empty; t has diverged entriely.
  • When you append to lookup2, it makes a separate allocation, since it wasn’t filled in with the allocation made by appending to lookup. It points to completely different memory than lookup.

This is also true of slices, but they aren’t often mutated in-place so it’s less apparent.

https://pkg.odin-lang.org/base/runtime/#Raw_Slice

Raw_Slice :: struct {
	data: rawptr, // effectively [^]T for []T
	len:  int,
}

Thank you for taking the time to write all of this for me! It made sense after you pointed out that dynamic arrays are just syntactic sugar for a struct. I was aware that Odin is pretty much an entirely a pass by value language. It just didn’t click for me that it was two different structs.

I was also aware of Slices having the same issue because of my brief experience with Go. Just never had the same issue with Go’s maps, but I never got that far into it.

Odin has slices and dynamic arrays be separate types for a reason. Unlike Go, Odin does not have garbage collection AND supports custom allocations. So as a result, a dynamic array and a slice should not be considered to be the same thing in practice.

As for maps in Odin, Go’s maps are a pointer to a implementation-defined data structure, whilst Odin’s are just a structure. Odin is very similar to what you might do in C or C++, and that’s it. Don’t try to overthink it. Odin is that dumb.