diff --git a/code/demo.odin b/code/demo.odin index 9a4db13cc..b1d1e1490 100644 --- a/code/demo.odin +++ b/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(); } +} 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}; + 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(); 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 *build_flags, BuildFlagKind kind, String name, Bu bool parse_build_flags(Array args) { Array 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 flag_args = args; @@ -291,27 +292,64 @@ bool parse_build_flags(Array 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 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 files; Array 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 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; 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; } +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 bool operator == (String a, char const (&b)[N]) { return str_eq(a, make_string(cast(u8 *)b, N-1)); } -template bool operator != (String a, char const (&b)[N]) { return str_ne(a, make_string(cast(u8 *)b, N-1)); } -template bool operator < (String a, char const (&b)[N]) { return str_lt(a, make_string(cast(u8 *)b, N-1)); } -template bool operator > (String a, char const (&b)[N]) { return str_gt(a, make_string(cast(u8 *)b, N-1)); } -template bool operator <= (String a, char const (&b)[N]) { return str_le(a, make_string(cast(u8 *)b, N-1)); } -template bool operator >= (String a, char const (&b)[N]) { return str_ge(a, make_string(cast(u8 *)b, N-1)); } +template bool operator == (String const &a, char const (&b)[N]) { return str_eq(a, make_string(cast(u8 *)b, N-1)); } +template bool operator != (String const &a, char const (&b)[N]) { return str_ne(a, make_string(cast(u8 *)b, N-1)); } +template bool operator < (String const &a, char const (&b)[N]) { return str_lt(a, make_string(cast(u8 *)b, N-1)); } +template bool operator > (String const &a, char const (&b)[N]) { return str_gt(a, make_string(cast(u8 *)b, N-1)); } +template bool operator <= (String const &a, char const (&b)[N]) { return str_le(a, make_string(cast(u8 *)b, N-1)); } +template bool operator >= (String const &a, char const (&b)[N]) { return str_ge(a, make_string(cast(u8 *)b, N-1)); }