Parallelization of the Parser

~66% reduction (unoptimized build)
~30% reduction (optimized build)
This commit is contained in:
Ginger Bill
2017-07-30 19:01:02 +01:00
parent 62a72f0163
commit 629b248f53
7 changed files with 312 additions and 130 deletions

View File

@@ -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();
}
}

View File

@@ -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};
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));
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();

View File

@@ -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) {

View File

@@ -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--;

View File

@@ -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;
}
}

View File

@@ -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 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));
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 (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;
for_array(i, p->imports) {
gbThread *t = &worker_threads[i];
gb_thread_init(t);
}
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);
// 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++;
}
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;
}
}
if (!are_any_alive && p->curr_import_index >= p->imports.count) {
break;
}
}
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;

View File

@@ -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; }
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; }
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); }
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 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)); }
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)); }