aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGinger Bill <bill@gingerbill.org>2017-07-30 19:01:02 +0100
committerGinger Bill <bill@gingerbill.org>2017-07-30 19:01:02 +0100
commit629b248f538b964c0480d40a1e5e7ad913ff6ea0 (patch)
treea932eeadf75d568c039898c1560dc76b19cdac57 /src
parent62a72f0163b2f35ca11cd8f4bbb4c7de2c66fca4 (diff)
Parallelization of the Parser
~66% reduction (unoptimized build) ~30% reduction (optimized build)
Diffstat (limited to 'src')
-rw-r--r--src/build_settings.cpp35
-rw-r--r--src/checker.cpp2
-rw-r--r--src/gb/gb.h34
-rw-r--r--src/main.cpp64
-rw-r--r--src/parser.cpp234
-rw-r--r--src/string.cpp57
6 files changed, 302 insertions, 124 deletions
diff --git a/src/build_settings.cpp b/src/build_settings.cpp
index 4b5ffdc64..07b7a8e3e 100644
--- a/src/build_settings.cpp
+++ b/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();
diff --git a/src/checker.cpp b/src/checker.cpp
index c243c704b..f6c20c048 100644
--- a/src/checker.cpp
+++ b/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) {
diff --git a/src/gb/gb.h b/src/gb/gb.h
index e613100bb..f1cc17b6b 100644
--- a/src/gb/gb.h
+++ b/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--;
diff --git a/src/main.cpp b/src/main.cpp
index abee3e4b0..0dcf16fc2 100644
--- a/src/main.cpp
+++ b/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;
+ }
}
diff --git a/src/parser.cpp b/src/parser.cpp
index 9ef0759ef..e95b606b1 100644
--- a/src/parser.cpp
+++ b/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;
diff --git a/src/string.cpp b/src/string.cpp
index 882bc273c..f9aaa8e11 100644
--- a/src/string.cpp
+++ b/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)); }