Browse Source

Parallelization of the Parser
~66% reduction (unoptimized build)
~30% reduction (optimized build)

Ginger Bill 8 years ago
parent
commit
629b248f53
7 changed files with 311 additions and 129 deletions
  1. 9 5
      code/demo.odin
  2. 24 11
      src/build_settings.cpp
  3. 2 0
      src/checker.cpp
  4. 24 10
      src/gb/gb.h
  5. 51 13
      src/main.cpp
  6. 168 66
      src/parser.cpp
  7. 33 24
      src/string.cpp

+ 9 - 5
code/demo.odin

@@ -338,7 +338,7 @@ union_type :: proc() {
 
 parametric_polymorphism :: proc() {
 	print_value :: proc(value: $T) {
-		fmt.printf("print_value: %v %v\n", value, value);
+		fmt.printf("print_value: %T %v\n", value, value);
 	}
 
 	v1: int    = 1;
@@ -496,10 +496,12 @@ parametric_polymorphism :: proc() {
 			return -1;
 		}
 
-		get_hash :: proc(s: string) -> u32 { // djb2
-			hash: u32 = 0x1505;
-			for i in 0..len(s) do hash = (hash<<5) + hash + u32(s[i]);
-			return hash;
+		get_hash :: proc(s: string) -> u32 { // fnv32a
+			h: u32 = 0x811c9dc5;
+			for i in 0..len(s) {
+				h = (h ~ u32(s[i])) * 0x01000193;
+			}
+			return h;
 		}
 
 
@@ -586,6 +588,7 @@ threading_example :: proc() {
 
 
 main :: proc() {
+when false {
 	if true {
 		fmt.println("\ngeneral_stuff:");              general_stuff();
 		fmt.println("\nnested_struct_declarations:"); nested_struct_declarations();
@@ -595,4 +598,5 @@ main :: proc() {
 	}
 	fmt.println("\nthreading_example:");           threading_example();
 }
+}
 

+ 24 - 11
src/build_settings.cpp

@@ -19,6 +19,9 @@ struct BuildContext {
 	bool   generate_docs;
 	i32    optimization_level;
 	bool   show_timings;
+
+	gbAffinity affinity;
+	isize      thread_count;
 };
 
 
@@ -205,18 +208,22 @@ String odin_root_dir(void) {
 
 #if defined(GB_SYSTEM_WINDOWS)
 String path_to_fullpath(gbAllocator a, String s) {
-	gbTempArenaMemory tmp = gb_temp_arena_memory_begin(&string_buffer_arena);
-	String16 string16 = string_to_string16(string_buffer_allocator, s);
-	String result = {0};
-
-	DWORD len = GetFullPathNameW(&string16[0], 0, nullptr, nullptr);
-	if (len != 0) {
-		wchar_t *text = gb_alloc_array(string_buffer_allocator, wchar_t, len+1);
-		GetFullPathNameW(&string16[0], len, text, nullptr);
-		text[len] = 0;
-		result = string16_to_string(a, make_string16(text, len));
+	String result = {};
+	gb_mutex_lock(&string_buffer_mutex);
+	{
+		gbTempArenaMemory tmp = gb_temp_arena_memory_begin(&string_buffer_arena);
+		String16 string16 = string_to_string16(string_buffer_allocator, s);
+
+		DWORD len = GetFullPathNameW(&string16[0], 0, nullptr, nullptr);
+		if (len != 0) {
+			wchar_t *text = gb_alloc_array(string_buffer_allocator, wchar_t, len+1);
+			GetFullPathNameW(&string16[0], len, text, nullptr);
+			text[len] = 0;
+			result = string16_to_string(a, make_string16(text, len));
+		}
+		gb_temp_arena_memory_end(tmp);
 	}
-	gb_temp_arena_memory_end(tmp);
+	gb_mutex_unlock(&string_buffer_mutex);
 	return result;
 }
 #elif defined(GB_SYSTEM_OSX) || defined(GB_SYSTEM_UNIX)
@@ -271,6 +278,12 @@ String const ODIN_VERSION = str_lit("0.6.0");
 
 void init_build_context(void) {
 	BuildContext *bc = &build_context;
+
+	gb_affinity_init(&bc->affinity);
+	if (bc->thread_count == 0) {
+		bc->thread_count = gb_max(bc->affinity.thread_count, 1);
+	}
+
 	bc->ODIN_VENDOR  = str_lit("odin");
 	bc->ODIN_VERSION = ODIN_VERSION;
 	bc->ODIN_ROOT    = odin_root_dir();

+ 2 - 0
src/checker.cpp

@@ -2279,6 +2279,8 @@ void check_parsed_files(Checker *c) {
 		scope->file      = f;
 		if (f->tokenizer.fullpath == c->parser->init_fullpath) {
 			scope->is_init = true;
+		} else if (f->file_kind == ImportedFile_Init) {
+			scope->is_init = true;
 		}
 
 		if (scope->is_global) {

+ 24 - 10
src/gb/gb.h

@@ -957,7 +957,7 @@ gb_mutex_init(&m);
 
 
 
-#define GB_THREAD_PROC(name) void name(void *data)
+#define GB_THREAD_PROC(name) isize name(struct gbThread *thread)
 typedef GB_THREAD_PROC(gbThreadProc);
 
 typedef struct gbThread {
@@ -968,7 +968,9 @@ typedef struct gbThread {
 #endif
 
 	gbThreadProc *proc;
-	void *        data;
+	void *        user_data;
+	isize         user_index;
+	isize         return_value;
 
 	gbSemaphore   semaphore;
 	isize         stack_size;
@@ -4672,22 +4674,32 @@ void gb_thread_destory(gbThread *t) {
 
 gb_inline void gb__thread_run(gbThread *t) {
 	gb_semaphore_release(&t->semaphore);
-	t->proc(t->data);
+	t->return_value = t->proc(t);
 }
 
 #if defined(GB_SYSTEM_WINDOWS)
-	gb_inline DWORD __stdcall gb__thread_proc(void *arg) { gb__thread_run(cast(gbThread *)arg); return 0; }
+	gb_inline DWORD __stdcall gb__thread_proc(void *arg) {
+		gbThread *t = cast(gbThread *)arg;
+		gb__thread_run(t);
+		t->is_running = false;
+		return 0;
+	}
 #else
-	gb_inline void *          gb__thread_proc(void *arg) { gb__thread_run(cast(gbThread *)arg); return NULL; }
+	gb_inline void *          gb__thread_proc(void *arg) {
+		gbThread *t = cast(gbThread *)arg;
+		gb__thread_run(t);
+		t->is_running = false;
+		return NULL;
+	}
 #endif
 
-gb_inline void gb_thread_start(gbThread *t, gbThreadProc *proc, void *data) { gb_thread_start_with_stack(t, proc, data, 0); }
+gb_inline void gb_thread_start(gbThread *t, gbThreadProc *proc, void *user_data) { gb_thread_start_with_stack(t, proc, user_data, 0); }
 
-gb_inline void gb_thread_start_with_stack(gbThread *t, gbThreadProc *proc, void *data, isize stack_size) {
+gb_inline void gb_thread_start_with_stack(gbThread *t, gbThreadProc *proc, void *user_data, isize stack_size) {
 	GB_ASSERT(!t->is_running);
 	GB_ASSERT(proc != NULL);
 	t->proc = proc;
-	t->data = data;
+	t->user_data = user_data;
 	t->stack_size = stack_size;
 
 #if defined(GB_SYSTEM_WINDOWS)
@@ -4698,8 +4710,9 @@ gb_inline void gb_thread_start_with_stack(gbThread *t, gbThreadProc *proc, void
 		pthread_attr_t attr;
 		pthread_attr_init(&attr);
 		pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
-		if (stack_size != 0)
+		if (stack_size != 0) {
 			pthread_attr_setstacksize(&attr, stack_size);
+		}
 		pthread_create(&t->posix_handle, &attr, gb__thread_proc, t);
 		pthread_attr_destroy(&attr);
 	}
@@ -5401,7 +5414,8 @@ gb_inline gbTempArenaMemory gb_temp_arena_memory_begin(gbArena *arena) {
 }
 
 gb_inline void gb_temp_arena_memory_end(gbTempArenaMemory tmp) {
-	GB_ASSERT(tmp.arena->total_allocated >= tmp.original_count);
+	GB_ASSERT_MSG(tmp.arena->total_allocated >= tmp.original_count,
+	              "%td >= %td", tmp.arena->total_allocated, tmp.original_count);
 	GB_ASSERT(tmp.arena->temp_count > 0);
 	tmp.arena->total_allocated = tmp.original_count;
 	tmp.arena->temp_count--;

+ 51 - 13
src/main.cpp

@@ -172,6 +172,7 @@ enum BuildFlagKind {
 
 	BuildFlag_OptimizationLevel,
 	BuildFlag_ShowTimings,
+	BuildFlag_ThreadCount,
 
 	BuildFlag_COUNT,
 };
@@ -202,9 +203,9 @@ void add_flag(Array<BuildFlag> *build_flags, BuildFlagKind kind, String name, Bu
 bool parse_build_flags(Array<String> args) {
 	Array<BuildFlag> build_flags = {};
 	array_init(&build_flags, heap_allocator(), BuildFlag_COUNT);
-	add_flag(&build_flags, BuildFlag_OptimizationLevel, str_lit("opt"), BuildFlagParam_Integer);
-	add_flag(&build_flags, BuildFlag_ShowTimings, str_lit("show-timings"), BuildFlagParam_None);
-
+	add_flag(&build_flags, BuildFlag_OptimizationLevel, str_lit("opt"),          BuildFlagParam_Integer);
+	add_flag(&build_flags, BuildFlag_ShowTimings,       str_lit("show-timings"), BuildFlagParam_None);
+	add_flag(&build_flags, BuildFlag_ThreadCount,       str_lit("thread-count"), BuildFlagParam_Integer);
 
 
 	Array<String> flag_args = args;
@@ -291,27 +292,64 @@ bool parse_build_flags(Array<String> args) {
 						}
 					}
 					if (ok) {
-						switch (bf.kind) {
-						case BuildFlag_OptimizationLevel:
-							if (value.kind == ExactValue_Integer) {
-								build_context.optimization_level = cast(i32)i128_to_i64(value.value_integer);
-							} else {
+						switch (bf.param_kind) {
+						case BuildFlagParam_None:
+							if (value.kind != ExactValue_Invalid) {
+								gb_printf_err("%.*s expected no value, got %.*s", LIT(name), LIT(param));
+								bad_flags = true;
+								ok = false;
+							}
+							break;
+						case BuildFlagParam_Boolean:
+							if (value.kind != ExactValue_Bool) {
+								gb_printf_err("%.*s expected a boolean, got %.*s", LIT(name), LIT(param));
+								bad_flags = true;
+								ok = false;
+							}
+							break;
+						case BuildFlagParam_Integer:
+							if (value.kind != ExactValue_Integer) {
 								gb_printf_err("%.*s expected an integer, got %.*s", LIT(name), LIT(param));
 								bad_flags = true;
 								ok = false;
 							}
 							break;
-						case BuildFlag_ShowTimings:
-							if (value.kind == ExactValue_Invalid) {
-								build_context.show_timings = true;
-							} else {
-								gb_printf_err("%.*s expected no value, got %.*s", LIT(name), LIT(param));
+						case BuildFlagParam_Float:
+							if (value.kind != ExactValue_Float) {
+								gb_printf_err("%.*s expected a floating pointer number, got %.*s", LIT(name), LIT(param));
+								bad_flags = true;
+								ok = false;
+							}
+							break;
+						case BuildFlagParam_String:
+							if (value.kind != ExactValue_String) {
+								gb_printf_err("%.*s expected a string, got %.*s", LIT(name), LIT(param));
 								bad_flags = true;
 								ok = false;
 							}
 							break;
 						}
 
+						if (ok) switch (bf.kind) {
+						case BuildFlag_OptimizationLevel:
+							GB_ASSERT(value.kind == ExactValue_Integer);
+							build_context.optimization_level = cast(i32)i128_to_i64(value.value_integer);
+							break;
+						case BuildFlag_ShowTimings:
+							GB_ASSERT(value.kind == ExactValue_Invalid);
+							build_context.show_timings = true;
+							break;
+						case BuildFlag_ThreadCount: {
+							GB_ASSERT(value.kind == ExactValue_Integer);
+							isize count = cast(isize)i128_to_i64(value.value_integer);
+							if (count <= 0) {
+								gb_printf_err("%.*s expected a positive non-zero number, got %.*s", LIT(name), LIT(param));
+								build_context.thread_count = 0;
+							} else {
+								build_context.thread_count = count;
+							}
+						} break;
+						}
 					}
 
 

+ 168 - 66
src/parser.cpp

@@ -20,6 +20,21 @@ struct CommentGroup {
 };
 
 
+enum ImportedFileKind {
+	ImportedFile_Normal,
+	ImportedFile_Shared,
+	ImportedFile_Init,
+};
+
+struct ImportedFile {
+	ImportedFileKind kind;
+	String           path;
+	String           rel_path;
+	TokenPos         pos; // import
+	isize            index;
+};
+
+
 struct AstFile {
 	i32            id;
 	gbArena        arena;
@@ -38,6 +53,7 @@ struct AstFile {
 	bool           allow_type;
 
 	Array<AstNode *> decls;
+	ImportedFileKind file_kind;
 	bool             is_global_scope;
 
 	AstNode *      curr_proc;
@@ -58,16 +74,12 @@ struct AstFile {
 	TokenPos fix_prev_pos;
 };
 
-struct ImportedFile {
-	String   path;
-	String   rel_path;
-	TokenPos pos; // import
-};
 
 struct Parser {
 	String              init_fullpath;
 	Array<AstFile>      files;
 	Array<ImportedFile> imports;
+	isize               curr_import_index;
 	gbAtomic32          import_index;
 	isize               total_token_count;
 	isize               total_line_count;
@@ -4748,18 +4760,19 @@ ParseFileError init_ast_file(AstFile *f, String fullpath) {
 	}
 	TokenizerInitError err = init_tokenizer(&f->tokenizer, fullpath);
 	if (err == TokenizerInit_None) {
-		array_init(&f->tokens, heap_allocator());
-		{
-			for (;;) {
-				Token token = tokenizer_get_token(&f->tokenizer);
-				if (token.kind == Token_Invalid) {
-					return ParseFile_InvalidToken;
-				}
-				array_add(&f->tokens, token);
+		isize file_size = f->tokenizer.end - f->tokenizer.start;
+		isize init_token_cap = gb_max(next_pow2(file_size/2), 16);
+		array_init(&f->tokens, heap_allocator(), gb_max(init_token_cap, 16));
 
-				if (token.kind == Token_EOF) {
-					break;
-				}
+		for (;;) {
+			Token token = tokenizer_get_token(&f->tokenizer);
+			if (token.kind == Token_Invalid) {
+				return ParseFile_InvalidToken;
+			}
+			array_add(&f->tokens, token);
+
+			if (token.kind == Token_EOF) {
+				break;
 			}
 		}
 
@@ -4821,7 +4834,6 @@ void destroy_parser(Parser *p) {
 
 // NOTE(bill): Returns true if it's added
 bool try_add_import_path(Parser *p, String path, String rel_path, TokenPos pos) {
-
 	gb_mutex_lock(&p->mutex);
 	defer (gb_mutex_unlock(&p->mutex));
 
@@ -4839,10 +4851,12 @@ bool try_add_import_path(Parser *p, String path, String rel_path, TokenPos pos)
 		}
 	}
 
-	ImportedFile item;
-	item.path = path;
+	ImportedFile item = {};
+	item.kind     = ImportedFile_Normal;
+	item.path     = path;
 	item.rel_path = rel_path;
-	item.pos = pos;
+	item.pos      = pos;
+	item.index    = p->imports.count;
 	array_add(&p->imports, item);
 
 
@@ -4979,80 +4993,168 @@ void parse_file(Parser *p, AstFile *f) {
 
 
 
+ParseFileError parse_import(Parser *p, ImportedFile imported_file) {
+	String import_path = imported_file.path;
+	String import_rel_path = imported_file.rel_path;
+	TokenPos pos = imported_file.pos;
+	AstFile file = {};
+	file.file_kind = imported_file.kind;
+	if (file.file_kind == ImportedFile_Shared) {
+		file.is_global_scope = true;
+	}
+
+	ParseFileError err = init_ast_file(&file, import_path);
+
+	if (err != ParseFile_None) {
+		if (err == ParseFile_EmptyFile) {
+			if (import_path == p->init_fullpath) {
+				gb_printf_err("Initial file is empty - %.*s\n", LIT(p->init_fullpath));
+				gb_exit(1);
+			}
+			return ParseFile_None;
+		}
+
+		if (pos.line != 0) {
+			gb_printf_err("%.*s(%td:%td) ", LIT(pos.file), pos.line, pos.column);
+		}
+		gb_printf_err("Failed to parse file: %.*s\n\t", LIT(import_rel_path));
+		switch (err) {
+		case ParseFile_WrongExtension:
+			gb_printf_err("Invalid file extension: File must have the extension `.odin`");
+			break;
+		case ParseFile_InvalidFile:
+			gb_printf_err("Invalid file or cannot be found");
+			break;
+		case ParseFile_Permission:
+			gb_printf_err("File permissions problem");
+			break;
+		case ParseFile_NotFound:
+			gb_printf_err("File cannot be found (`%.*s`)", LIT(import_path));
+			break;
+		case ParseFile_InvalidToken:
+			gb_printf_err("Invalid token found in file");
+			break;
+		}
+		gb_printf_err("\n");
+		return err;
+	}
+	parse_file(p, &file);
+
+	{
+		gb_mutex_lock(&p->mutex);
+		file.id = imported_file.index;
+		array_add(&p->files, file);
+		p->total_line_count += file.tokenizer.line_count;
+		gb_mutex_unlock(&p->mutex);
+	}
+
+
+	return ParseFile_None;
+}
+
+GB_THREAD_PROC(parse_worker_file_proc) {
+	if (thread == nullptr) return 0;
+	auto *p = cast(Parser *)thread->user_data;
+	isize index = thread->user_index;
+	ImportedFile imported_file = p->imports[index];
+	ParseFileError err = parse_import(p, imported_file);
+	return cast(isize)err;
+}
+
+
+struct ParserThreadWork {
+	Parser *parser;
+	isize   import_index;
+};
+
 ParseFileError parse_files(Parser *p, String init_filename) {
 	GB_ASSERT(init_filename.text[init_filename.len] == 0);
 
 	char *fullpath_str = gb_path_get_full_name(heap_allocator(), cast(char *)&init_filename[0]);
 	String init_fullpath = string_trim_whitespace(make_string_c(fullpath_str));
 	TokenPos init_pos = {};
-	ImportedFile init_imported_file = {init_fullpath, init_fullpath, init_pos};
+	ImportedFile init_imported_file = {ImportedFile_Init, init_fullpath, init_fullpath, init_pos};
 
+	isize shared_file_count = 0;
 	if (!build_context.generate_docs) {
 		String s = get_fullpath_core(heap_allocator(), str_lit("_preload.odin"));
-		ImportedFile runtime_file = {s, s, init_pos};
+		ImportedFile runtime_file = {ImportedFile_Shared, s, s, init_pos};
 		array_add(&p->imports, runtime_file);
+		shared_file_count++;
 	}
 	if (!build_context.generate_docs) {
 		String s = get_fullpath_core(heap_allocator(), str_lit("_soft_numbers.odin"));
-		ImportedFile runtime_file = {s, s, init_pos};
+		ImportedFile runtime_file = {ImportedFile_Shared, s, s, init_pos};
 		array_add(&p->imports, runtime_file);
+		shared_file_count++;
 	}
-
 	array_add(&p->imports, init_imported_file);
 	p->init_fullpath = init_fullpath;
 
-	for_array(i, p->imports) {
-		ImportedFile imported_file = p->imports[i];
-		String import_path = imported_file.path;
-		String import_rel_path = imported_file.rel_path;
-		TokenPos pos = imported_file.pos;
-		AstFile file = {};
-
-		ParseFileError err = init_ast_file(&file, import_path);
 
-		if (err != ParseFile_None) {
-			if (err == ParseFile_EmptyFile) {
-				if (import_path == init_fullpath) {
-					gb_printf_err("Initial file is empty - %.*s\n", LIT(init_fullpath));
-					gb_exit(1);
-				}
-				return ParseFile_None;
+#if 1
+	isize thread_count = gb_max(build_context.thread_count, 1);
+	if (thread_count > 1) {
+		Array<gbThread> worker_threads = {};
+		array_init_count(&worker_threads, heap_allocator(), thread_count);
+		defer (array_free(&worker_threads));
+
+		for_array(i, p->imports) {
+			gbThread *t = &worker_threads[i];
+			gb_thread_init(t);
+		}
+
+		// NOTE(bill): Make sure that these are in parsed in this order
+		for (isize i = 0; i < shared_file_count; i++) {
+			ParseFileError err = parse_import(p, p->imports[i]);
+			if (err != ParseFile_None) {
+				return err;
 			}
+			p->curr_import_index++;
+		}
 
-			if (pos.line != 0) {
-				gb_printf_err("%.*s(%td:%td) ", LIT(pos.file), pos.line, pos.column);
+		for (;;) {
+			bool are_any_alive = false;
+			for_array(i, worker_threads) {
+				gbThread *t = &worker_threads[i];
+				if (gb_thread_is_running(t)) {
+					are_any_alive = true;
+				} else if (p->curr_import_index < p->imports.count) {
+					if (t->return_value != 0) {
+						for_array(i, worker_threads) {
+							gb_thread_destory(&worker_threads[i]);
+						}
+						return cast(ParseFileError)t->return_value;
+					}
+					t->user_index = p->curr_import_index++;
+					gb_thread_start(t, parse_worker_file_proc, p);
+					are_any_alive = true;
+				}
 			}
-			gb_printf_err("Failed to parse file: %.*s\n\t", LIT(import_rel_path));
-			switch (err) {
-			case ParseFile_WrongExtension:
-				gb_printf_err("Invalid file extension: File must have the extension `.odin`");
-				break;
-			case ParseFile_InvalidFile:
-				gb_printf_err("Invalid file or cannot be found");
-				break;
-			case ParseFile_Permission:
-				gb_printf_err("File permissions problem");
-				break;
-			case ParseFile_NotFound:
-				gb_printf_err("File cannot be found (`%.*s`)", LIT(import_path));
-				break;
-			case ParseFile_InvalidToken:
-				gb_printf_err("Invalid token found in file");
+			if (!are_any_alive && p->curr_import_index >= p->imports.count) {
 				break;
 			}
-			gb_printf_err("\n");
-			return err;
 		}
-		parse_file(p, &file);
 
-		{
-			gb_mutex_lock(&p->mutex);
-			file.id = p->files.count;
-			array_add(&p->files, file);
-			p->total_line_count += file.tokenizer.line_count;
-			gb_mutex_unlock(&p->mutex);
+		for_array(i, worker_threads) {
+			gb_thread_destory(&worker_threads[i]);
+		}
+	} else {
+		for_array(i, p->imports) {
+			ParseFileError err = parse_import(p, p->imports[i]);
+			if (err != ParseFile_None) {
+				return err;
+			}
+		}
+	}
+#else
+	for_array(i, p->imports) {
+		ParseFileError err = parse_import(p, p->imports[i]);
+		if (err != ParseFile_None) {
+			return err;
 		}
 	}
+#endif
 
 	for_array(i, p->files) {
 		p->total_token_count += p->files[i].tokens.count;

+ 33 - 24
src/string.cpp

@@ -1,10 +1,12 @@
-gb_global gbArena string_buffer_arena = {};
+gb_global gbArena     string_buffer_arena = {};
 gb_global gbAllocator string_buffer_allocator = {};
+gb_global gbMutex     string_buffer_mutex = {};
 
 void init_string_buffer_memory(void) {
 	// NOTE(bill): This should be enough memory for file systems
 	gb_arena_init_from_allocator(&string_buffer_arena, heap_allocator(), gb_megabytes(1));
 	string_buffer_allocator = gb_arena_allocator(&string_buffer_arena);
+	gb_mutex_init(&string_buffer_mutex);
 }
 
 
@@ -104,9 +106,8 @@ gb_inline bool str_eq_ignore_case(String a, String b) {
 	return false;
 }
 
-int string_compare(String x, String y) {
-	if (!(x.len == y.len &&
-	      x.text == y.text)) {
+int string_compare(String const &x, String const &y) {
+	if (x.len != y.len || x.text != y.text) {
 		isize n, fast, offset, curr_block;
 		isize *la, *lb;
 		isize pos;
@@ -148,26 +149,34 @@ GB_COMPARE_PROC(string_cmp_proc) {
 	return string_compare(x, y);
 }
 
-gb_inline bool str_eq(String a, String b) { return a.len == b.len ? gb_memcompare(a.text, b.text, a.len) == 0 : false; }
-gb_inline bool str_ne(String a, String b) { return !str_eq(a, b);                }
-gb_inline bool str_lt(String a, String b) { return string_compare(a, b) < 0;     }
-gb_inline bool str_gt(String a, String b) { return string_compare(a, b) > 0;     }
-gb_inline bool str_le(String a, String b) { return string_compare(a, b) <= 0;    }
-gb_inline bool str_ge(String a, String b) { return string_compare(a, b) >= 0;    }
-
-bool operator == (String a, String b) { return str_eq(a, b); }
-bool operator != (String a, String b) { return str_ne(a, b); }
-bool operator <  (String a, String b) { return str_lt(a, b); }
-bool operator >  (String a, String b) { return str_gt(a, b); }
-bool operator <= (String a, String b) { return str_le(a, b); }
-bool operator >= (String a, String b) { return str_ge(a, b); }
-
-template <isize N> bool operator == (String a, char const (&b)[N]) { return str_eq(a, make_string(cast(u8 *)b, N-1)); }
-template <isize N> bool operator != (String a, char const (&b)[N]) { return str_ne(a, make_string(cast(u8 *)b, N-1)); }
-template <isize N> bool operator <  (String a, char const (&b)[N]) { return str_lt(a, make_string(cast(u8 *)b, N-1)); }
-template <isize N> bool operator >  (String a, char const (&b)[N]) { return str_gt(a, make_string(cast(u8 *)b, N-1)); }
-template <isize N> bool operator <= (String a, char const (&b)[N]) { return str_le(a, make_string(cast(u8 *)b, N-1)); }
-template <isize N> bool operator >= (String a, char const (&b)[N]) { return str_ge(a, make_string(cast(u8 *)b, N-1)); }
+gb_inline bool str_eq(String const &a, String const &b) {
+	if (a.len != b.len) return false;
+	for (isize i = 0; i < a.len; i++) {
+		if (a.text[i] != b.text[i]) {
+			return false;
+		}
+	}
+	return true;
+}
+gb_inline bool str_ne(String const &a, String const &b) { return !str_eq(a, b);                }
+gb_inline bool str_lt(String const &a, String const &b) { return string_compare(a, b) < 0;     }
+gb_inline bool str_gt(String const &a, String const &b) { return string_compare(a, b) > 0;     }
+gb_inline bool str_le(String const &a, String const &b) { return string_compare(a, b) <= 0;    }
+gb_inline bool str_ge(String const &a, String const &b) { return string_compare(a, b) >= 0;    }
+
+gb_inline bool operator == (String const &a, String const &b) { return str_eq(a, b); }
+gb_inline bool operator != (String const &a, String const &b) { return str_ne(a, b); }
+gb_inline bool operator <  (String const &a, String const &b) { return str_lt(a, b); }
+gb_inline bool operator >  (String const &a, String const &b) { return str_gt(a, b); }
+gb_inline bool operator <= (String const &a, String const &b) { return str_le(a, b); }
+gb_inline bool operator >= (String const &a, String const &b) { return str_ge(a, b); }
+
+template <isize N> bool operator == (String const &a, char const (&b)[N]) { return str_eq(a, make_string(cast(u8 *)b, N-1)); }
+template <isize N> bool operator != (String const &a, char const (&b)[N]) { return str_ne(a, make_string(cast(u8 *)b, N-1)); }
+template <isize N> bool operator <  (String const &a, char const (&b)[N]) { return str_lt(a, make_string(cast(u8 *)b, N-1)); }
+template <isize N> bool operator >  (String const &a, char const (&b)[N]) { return str_gt(a, make_string(cast(u8 *)b, N-1)); }
+template <isize N> bool operator <= (String const &a, char const (&b)[N]) { return str_le(a, make_string(cast(u8 *)b, N-1)); }
+template <isize N> bool operator >= (String const &a, char const (&b)[N]) { return str_ge(a, make_string(cast(u8 *)b, N-1)); }