diff --git a/build.bat b/build.bat index ea9952610..92155e787 100644 --- a/build.bat +++ b/build.bat @@ -43,7 +43,7 @@ del *.ilk > NUL 2> NUL cl %compiler_settings% "src\main.cpp" ^ /link %linker_settings% -OUT:%exe_name% ^ - && odin run examples/demo.odin -opt=0 + && odin run examples/demo.odin -opt=0 -collection=foo=W:\Odin\core rem && odin docs core/fmt.odin del *.obj > NUL 2> NUL diff --git a/core/_preload.odin b/core/_preload.odin index f394ae0e5..b3808e551 100644 --- a/core/_preload.odin +++ b/core/_preload.odin @@ -1,9 +1,9 @@ #shared_global_scope; -import "os.odin"; -import "fmt.odin"; // TODO(bill): Remove the need for `fmt` here -import "utf8.odin"; -import "raw.odin"; +import "core:os.odin"; +import "core:fmt.odin"; // TODO(bill): Remove the need for `fmt` here +import "core:utf8.odin"; +import "core:raw.odin"; // Naming Conventions: // In general, Ada_Case for types and snake_case for values @@ -621,8 +621,6 @@ panic :: proc(message := "", using location := #caller_location) #cc_contextless } - - __string_eq :: proc(a, b: string) -> bool #cc_contextless { match { case len(a) != len(b): return false; diff --git a/core/atomics.odin b/core/atomics.odin index 8dd40da7a..99655a3dd 100644 --- a/core/atomics.odin +++ b/core/atomics.odin @@ -1,7 +1,7 @@ // TODO(bill): Use assembly instead here to implement atomics // Inline vs external file? -import win32 "sys/windows.odin" when ODIN_OS == "windows"; +import win32 "core:sys/windows.odin" when ODIN_OS == "windows"; _ :: compile_assert(ODIN_ARCH == "amd64"); // TODO(bill): x86 version diff --git a/core/fmt.odin b/core/fmt.odin index f7cd7e31f..6cb142aea 100644 --- a/core/fmt.odin +++ b/core/fmt.odin @@ -1,9 +1,9 @@ -import "os.odin"; -import "mem.odin"; -import "utf8.odin"; -import "types.odin"; -import "strconv.odin"; -import "raw.odin"; +import "core:os.odin"; +import "core:mem.odin"; +import "core:utf8.odin"; +import "core:types.odin"; +import "core:strconv.odin"; +import "core:raw.odin"; _BUFFER_SIZE :: 1<<12; diff --git a/core/hash.odin b/core/hash.odin index 44a4b69fd..d523a04e1 100644 --- a/core/hash.odin +++ b/core/hash.odin @@ -1,4 +1,4 @@ -import "mem.odin"; +import "core:mem.odin"; adler32 :: proc(data: []u8) -> u32 { ADLER_CONST :: 65521; diff --git a/core/mem.odin b/core/mem.odin index cce8492f6..dc11c84c6 100644 --- a/core/mem.odin +++ b/core/mem.odin @@ -1,6 +1,6 @@ -import "fmt.odin"; -import "os.odin"; -import "raw.odin"; +import "core:fmt.odin"; +import "core:os.odin"; +import "core:raw.odin"; foreign __llvm_core { swap :: proc(b: u16) -> u16 #link_name "llvm.bswap.i16" ---; diff --git a/core/opengl.odin b/core/opengl.odin index e4b1e117e..7bce13a85 100644 --- a/core/opengl.odin +++ b/core/opengl.odin @@ -1,9 +1,9 @@ foreign_system_library lib "opengl32.lib" when ODIN_OS == "windows"; foreign_system_library lib "gl" when ODIN_OS == "linux"; -import win32 "sys/windows.odin" when ODIN_OS == "windows"; -import "sys/wgl.odin" when ODIN_OS == "windows"; -export "opengl_constants.odin"; +import win32 "core:sys/windows.odin" when ODIN_OS == "windows"; +import "core:sys/wgl.odin" when ODIN_OS == "windows"; +export "core:opengl_constants.odin"; _ := compile_assert(ODIN_OS != "osx"); diff --git a/core/os.odin b/core/os.odin index 61dcf6ea3..ecf0139c9 100644 --- a/core/os.odin +++ b/core/os.odin @@ -1,6 +1,6 @@ -export "os_windows.odin" when ODIN_OS == "windows"; -export "os_x.odin" when ODIN_OS == "osx"; -export "os_linux.odin" when ODIN_OS == "linux"; +export "core:os_windows.odin" when ODIN_OS == "windows"; +export "core:os_x.odin" when ODIN_OS == "osx"; +export "core:os_linux.odin" when ODIN_OS == "linux"; write_string :: proc(fd: Handle, str: string) -> (int, Errno) { return write(fd, cast([]u8)str); diff --git a/core/os_linux.odin b/core/os_linux.odin index 240660a82..ddae24e9e 100644 --- a/core/os_linux.odin +++ b/core/os_linux.odin @@ -1,7 +1,7 @@ foreign_system_library dl "dl"; foreign_system_library libc "c"; -import "strings.odin"; +import "core:strings.odin"; Handle :: i32; File_Time :: u64; diff --git a/core/os_windows.odin b/core/os_windows.odin index 9af239555..103fd6b73 100644 --- a/core/os_windows.odin +++ b/core/os_windows.odin @@ -1,5 +1,5 @@ -import win32 "sys/windows.odin"; -import "mem.odin"; +import win32 "core:sys/windows.odin"; +import "core:mem.odin"; Handle :: int; File_Time :: u64; diff --git a/core/os_x.odin b/core/os_x.odin index 037558338..0c5510cd2 100644 --- a/core/os_x.odin +++ b/core/os_x.odin @@ -1,7 +1,7 @@ foreign_system_library dl "dl"; foreign_system_library libc "c"; -import "strings.odin"; +import "core:strings.odin"; Handle :: i32; File_Time :: u64; diff --git a/core/strconv.odin b/core/strconv.odin index 792835c8f..431b6a5ee 100644 --- a/core/strconv.odin +++ b/core/strconv.odin @@ -1,4 +1,4 @@ -using import "decimal.odin"; +using import "core:decimal.odin"; Int_Flag :: enum { Prefix = 1<<0, diff --git a/core/strings.odin b/core/strings.odin index dbe99f0e9..f2588da78 100644 --- a/core/strings.odin +++ b/core/strings.odin @@ -1,4 +1,4 @@ -import "mem.odin"; +import "core:mem.odin"; new_string :: proc(s: string) -> string { c := make([]u8, len(s)+1); diff --git a/core/sync.odin b/core/sync.odin index 689f54674..b57023bd1 100644 --- a/core/sync.odin +++ b/core/sync.odin @@ -1,2 +1,2 @@ -export "sync_windows.odin" when ODIN_OS == "windows"; -export "sync_linux.odin" when ODIN_OS == "linux"; +export "core:sync_windows.odin" when ODIN_OS == "windows"; +export "core:sync_linux.odin" when ODIN_OS == "linux"; diff --git a/core/sync_linux.odin b/core/sync_linux.odin index e38c80111..fcc8f7200 100644 --- a/core/sync_linux.odin +++ b/core/sync_linux.odin @@ -1,5 +1,5 @@ -import "atomics.odin"; -import "os.odin"; +import "core:atomics.odin"; +import "core:os.odin"; Semaphore :: struct { // _handle: win32.Handle; diff --git a/core/sync_windows.odin b/core/sync_windows.odin index fa270edfe..066cc95e8 100644 --- a/core/sync_windows.odin +++ b/core/sync_windows.odin @@ -1,5 +1,5 @@ -import win32 "sys/windows.odin" when ODIN_OS == "windows"; -import "atomics.odin"; +import win32 "core:sys/windows.odin" when ODIN_OS == "windows"; +import "core:atomics.odin"; Semaphore :: struct { _handle: win32.Handle; diff --git a/core/sys/wgl.odin b/core/sys/wgl.odin index 46e388057..65241a325 100644 --- a/core/sys/wgl.odin +++ b/core/sys/wgl.odin @@ -1,5 +1,5 @@ foreign_system_library "opengl32.lib" when ODIN_OS == "windows"; -using import "windows.odin"; +using import "core:sys/windows.odin"; CONTEXT_MAJOR_VERSION_ARB :: 0x2091; diff --git a/core/thread.odin b/core/thread.odin index 3510ad8af..e6e8ce53b 100644 --- a/core/thread.odin +++ b/core/thread.odin @@ -1,6 +1,6 @@ _ :: compile_assert(ODIN_OS == "windows"); -import win32 "sys/windows.odin"; +import win32 "core:sys/windows.odin"; Thread :: struct { using specific: Os_Specific; diff --git a/examples/demo.odin b/examples/demo.odin index de0dce69c..8af9bff38 100644 --- a/examples/demo.odin +++ b/examples/demo.odin @@ -1,21 +1,21 @@ -import "fmt.odin"; -import "strconv.odin"; -import "mem.odin"; -import "thread.odin" when ODIN_OS == "windows"; -import win32 "sys/windows.odin" when ODIN_OS == "windows"; -import "atomics.odin"; -import "bits.odin"; -import "hash.odin"; -import "math.odin"; -import "opengl.odin"; -import "os.odin"; -import "raw.odin"; -import "sort.odin"; -import "strings.odin"; -import "sync.odin"; -import "types.odin"; -import "utf8.odin"; -import "utf16.odin"; +import "core:fmt.odin"; +import "core:strconv.odin"; +import "core:mem.odin"; +import "core:thread.odin" when ODIN_OS == "windows"; +import win32 "core:sys/windows.odin" when ODIN_OS == "windows"; +import "core:atomics.odin"; +import "core:bits.odin"; +import "core:hash.odin"; +import "core:math.odin"; +import "core:opengl.odin"; +import "core:os.odin"; +import "core:raw.odin"; +import "core:sort.odin"; +import "core:strings.odin"; +import "core:sync.odin"; +import "core:types.odin"; +import "core:utf8.odin"; +import "core:utf16.odin"; general_stuff :: proc() { { // `do` for inline statmes rather than block diff --git a/shared/.gitkeep b/shared/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/build_settings.cpp b/src/build_settings.cpp index a63ff7b4e..b6846a9f6 100644 --- a/src/build_settings.cpp +++ b/src/build_settings.cpp @@ -29,7 +29,28 @@ struct BuildContext { gb_global BuildContext build_context = {0}; +struct LibraryCollections { + String name; + String path; +}; +gb_global Array library_collections = {0}; + +void add_library_collection(String name, String path) { + // TODO(bill): Check the path is valid and a directory + LibraryCollections lc = {name, string_trim_whitespace(path)}; + array_add(&library_collections, lc); +} + +bool find_library_collection_path(String name, String *path) { + for_array(i, library_collections) { + if (library_collections[i].name == name) { + if (path) *path = library_collections[i].path; + return true; + } + } + return false; +} // TODO(bill): OS dependent versions for the BuildContext @@ -248,38 +269,39 @@ String path_to_fullpath(gbAllocator a, String s) { String get_fullpath_relative(gbAllocator a, String base_dir, String path) { - String res = {0}; - isize str_len = base_dir.len+path.len; - - u8 *str = gb_alloc_array(heap_allocator(), u8, str_len+1); + u8 *str = gb_alloc_array(heap_allocator(), u8, base_dir.len+1+path.len+1); + defer (gb_free(heap_allocator(), str)); isize i = 0; - gb_memmove(str+i, &base_dir[0], base_dir.len); i += base_dir.len; - gb_memmove(str+i, &path[0], path.len); - str[str_len] = '\0'; - res = path_to_fullpath(a, make_string(str, str_len)); - gb_free(heap_allocator(), str); - return res; + gb_memmove(str+i, base_dir.text, base_dir.len); i += base_dir.len; + gb_memmove(str+i, "/", 1); i += 1; + gb_memmove(str+i, path.text, path.len); i += path.len; + str[i] = 0; + + String res = make_string(str, i); + res = string_trim_whitespace(res); + return path_to_fullpath(a, res); } + String get_fullpath_core(gbAllocator a, String path) { String module_dir = odin_root_dir(); - String res = {0}; - char core[] = "core/"; - isize core_len = gb_size_of(core)-1; + String core = str_lit("core/"); - isize str_len = module_dir.len + core_len + path.len; + isize str_len = module_dir.len + core.len + path.len; u8 *str = gb_alloc_array(heap_allocator(), u8, str_len+1); + defer (gb_free(heap_allocator(), str)); - gb_memmove(str, &module_dir[0], module_dir.len); - gb_memmove(str+module_dir.len, core, core_len); - gb_memmove(str+module_dir.len+core_len, &path[0], path.len); - str[str_len] = '\0'; + isize i = 0; + gb_memmove(str+i, module_dir.text, module_dir.len); i += module_dir.len; + gb_memmove(str+i, core.text, core.len); i += core.len; + gb_memmove(str+i, path.text, path.len); i += path.len; + str[i] = 0; - res = path_to_fullpath(a, make_string(str, str_len)); - gb_free(heap_allocator(), str); - return res; + String res = make_string(str, i); + res = string_trim_whitespace(res); + return path_to_fullpath(a, res); } diff --git a/src/common.cpp b/src/common.cpp index 06cb674dd..173ef4210 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -463,3 +463,31 @@ wchar_t **command_line_to_wargv(wchar_t *cmd_line, int *_argc) { } #endif + + +#if defined(GB_SYSTEM_WINDOWS) + bool path_is_directory(String path) { + gbAllocator a = heap_allocator(); + String16 wstr = string_to_string16(a, path); + defer (gb_free(a, wstr.text)); + + i32 attribs = GetFileAttributesW(wstr.text); + if (attribs < 0) return false; + + return (attribs & FILE_ATTRIBUTE_DIRECTORY) != 0; + } + +#else + bool path_is_directory(String path) { + gbAllocator a = heap_allocator(); + String copy = copy_string(a, path); + defer (gb_free(a, copy.text)); + + struct stat s; + if (stat(copy.text, &s) == 0) { + return (s.st_mode & S_IFDIR) != 0; + } + return false; + } +#endif + diff --git a/src/main.cpp b/src/main.cpp index 0301b1186..eda4de2d1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -177,6 +177,7 @@ enum BuildFlagKind { BuildFlag_ShowTimings, BuildFlag_ThreadCount, BuildFlag_KeepTempFiles, + BuildFlag_Collection, BuildFlag_COUNT, }; @@ -204,6 +205,36 @@ void add_flag(Array *build_flags, BuildFlagKind kind, String name, Bu array_add(build_flags, flag); } +bool string_is_valid_identifier(String str) { + if (str.len <= 0) return false; + + isize rune_count = 0; + + isize w = 0; + isize offset = 0; + while (offset < str.len) { + Rune r = 0; + w = gb_utf8_decode(str.text, str.len, &r); + if (r == GB_RUNE_INVALID) { + return false; + } + + if (rune_count == 0) { + if (!rune_is_letter(r)) { + return false; + } + } else { + if (!rune_is_letter(r) && !rune_is_digit(r)) { + return false; + } + } + rune_count += 1; + offset += w; + } + + return true; +} + bool parse_build_flags(Array args) { Array build_flags = {}; array_init(&build_flags, heap_allocator(), BuildFlag_COUNT); @@ -211,6 +242,7 @@ bool parse_build_flags(Array args) { add_flag(&build_flags, BuildFlag_ShowTimings, str_lit("show-timings"), BuildFlagParam_None); add_flag(&build_flags, BuildFlag_ThreadCount, str_lit("thread-count"), BuildFlagParam_Integer); add_flag(&build_flags, BuildFlag_KeepTempFiles, str_lit("keep-temp-files"), BuildFlagParam_None); + add_flag(&build_flags, BuildFlag_Collection, str_lit("collection"), BuildFlagParam_String); Array flag_args = args; @@ -358,6 +390,68 @@ bool parse_build_flags(Array args) { GB_ASSERT(value.kind == ExactValue_Invalid); build_context.keep_temp_files = true; break; + + case BuildFlag_Collection: { + GB_ASSERT(value.kind == ExactValue_String); + String str = value.value_string; + isize eq_pos = -1; + for (isize i = 0; i < str.len; i++) { + if (str[i] == '=') { + eq_pos = i; + break; + } + } + if (eq_pos < 0) { + gb_printf_err("Expected `name=path`, got `%.*s`\n", LIT(param)); + ok = false; + bad_flags = true; + break; + } + String name = substring(str, 0, eq_pos); + String path = substring(str, eq_pos+1, str.len); + if (name.len == 0 || path.len == 0) { + gb_printf_err("Expected `name=path`, got `%.*s`\n", LIT(param)); + ok = false; + bad_flags = true; + break; + } + + if (!string_is_valid_identifier(name)) { + gb_printf_err("Library collection name `%.*s` must be a valid identifier\n", LIT(name)); + ok = false; + bad_flags = true; + break; + } + + if (name == "_") { + gb_printf_err("Library collection name cannot be an underscore\n"); + ok = false; + bad_flags = true; + break; + } + + String prev_path = {}; + bool found = find_library_collection_path(name, &prev_path); + if (found) { + gb_printf_err("Library collection `%.*s` already exists with path `%.*s`\n", LIT(name), LIT(prev_path)); + ok = false; + bad_flags = true; + break; + } + + gbAllocator a = heap_allocator(); + String fullpath = path_to_fullpath(a, path); + if (!path_is_directory(fullpath)) { + gb_printf_err("Library collection `%.*s` path must be a directory, got `%.*s`\n", LIT(name), LIT(fullpath)); + gb_free(a, fullpath.text); + ok = false; + bad_flags = true; + break; + } + + add_library_collection(name, path); + + } break; } } @@ -449,6 +543,10 @@ int main(int arg_count, char **arg_ptr) { init_scratch_memory(gb_megabytes(10)); init_global_error_collector(); + array_init(&library_collections, heap_allocator()); + add_library_collection(str_lit("core"), get_fullpath_relative(heap_allocator(), odin_root_dir(), str_lit("core"))); + add_library_collection(str_lit("shared"), get_fullpath_relative(heap_allocator(), odin_root_dir(), str_lit("shared"))); + Array args = setup_args(arg_count, arg_ptr); #if 1 diff --git a/src/parser.cpp b/src/parser.cpp index e335888b8..e9e829457 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -4796,6 +4796,54 @@ bool is_import_path_valid(String path) { return false; } +bool determine_path_from_string(Parser *p, AstNode *node, String base_dir, String original_string, String *path) { + GB_ASSERT(path != nullptr); + + gbAllocator a = heap_allocator(); + String collection_name = {}; + + isize colon_pos = -1; + for (isize j = 0; j < original_string.len; j++) { + if (original_string[j] == ':') { + colon_pos = j; + break; + } + } + + String file_str = {}; + if (colon_pos == 0) { + syntax_error(node, "Expected a collection name"); + return false; + } + + if (original_string.len > 0 && colon_pos > 0) { + collection_name = substring(original_string, 0, colon_pos); + file_str = substring(original_string, colon_pos+1, original_string.len); + } else { + file_str = original_string; + } + + if (!is_import_path_valid(file_str)) { + syntax_error(node, "Invalid import path: `%.*s`", LIT(file_str)); + return false; + } + + gb_mutex_lock(&p->file_decl_mutex); + defer (gb_mutex_unlock(&p->file_decl_mutex)); + + if (collection_name.len > 0) { + if (!find_library_collection_path(collection_name, &base_dir)) { + // NOTE(bill): It's a naughty name + syntax_error(node, "Unknown library colleciton: `%.*s`", LIT(base_dir)); + return false; + } + } + String fullpath = string_trim_whitespace(get_fullpath_relative(a, base_dir, file_str)); + *path = fullpath; + + return true; +} + void parse_setup_file_decls(Parser *p, AstFile *f, String base_dir, Array decls) { for_array(i, decls) { AstNode *node = decls[i]; @@ -4803,73 +4851,38 @@ void parse_setup_file_decls(Parser *p, AstFile *f, String base_dir, Arraykind != AstNode_BadStmt && node->kind != AstNode_EmptyStmt) { // NOTE(bill): Sanity check - syntax_error(node, "Only declarations are allowed at file scope %.*s", LIT(ast_node_strings[node->kind])); + syntax_error(node, "Only declarations are allowed at file scope, got %.*s", LIT(ast_node_strings[node->kind])); } else if (node->kind == AstNode_ImportDecl) { ast_node(id, ImportDecl, node); - String collection_name = {}; - String oirignal_string = id->relpath.string; - String file_str = id->relpath.string; - gbAllocator a = heap_allocator(); // TODO(bill): Change this allocator - String import_file = {}; - String rel_path = {}; - if (!is_import_path_valid(file_str)) { - syntax_error(node, "Invalid import path: `%.*s`", LIT(file_str)); - // NOTE(bill): It's a naughty name + String original_string = id->relpath.string; + String import_path = {}; + bool ok = determine_path_from_string(p, node, base_dir, original_string, &import_path); + if (!ok) { decls[i] = ast_bad_decl(f, id->relpath, id->relpath); continue; } - gb_mutex_lock(&p->file_decl_mutex); - defer (gb_mutex_unlock(&p->file_decl_mutex)); - - rel_path = get_fullpath_relative(a, base_dir, file_str); - import_file = rel_path; - if (!gb_file_exists(cast(char *)rel_path.text)) { // NOTE(bill): This should be null terminated - String abs_path = get_fullpath_core(a, file_str); - if (gb_file_exists(cast(char *)abs_path.text)) { - import_file = abs_path; - } - } - - import_file = string_trim_whitespace(import_file); - - id->fullpath = import_file; - try_add_import_path(p, import_file, file_str, ast_node_token(node).pos); + id->fullpath = import_path; + try_add_import_path(p, import_path, original_string, ast_node_token(node).pos); } else if (node->kind == AstNode_ExportDecl) { ast_node(ed, ExportDecl, node); - String collection_name = {}; - String oirignal_string = ed->relpath.string; - String file_str = ed->relpath.string; - gbAllocator a = heap_allocator(); // TODO(bill): Change this allocator - String export_path = {}; - String rel_path = {}; - if (!is_import_path_valid(file_str)) { - syntax_error(node, "Invalid export path: `%.*s`", LIT(file_str)); - // NOTE(bill): It's a naughty name + String original_string = ed->relpath.string; + String export_path = {}; + bool ok = determine_path_from_string(p, node, base_dir, original_string, &export_path); + if (!ok) { decls[i] = ast_bad_decl(f, ed->relpath, ed->relpath); continue; } - gb_mutex_lock(&p->file_decl_mutex); - defer (gb_mutex_unlock(&p->file_decl_mutex)); - - rel_path = get_fullpath_relative(a, base_dir, file_str); - export_path = rel_path; - if (!gb_file_exists(cast(char *)rel_path.text)) { // NOTE(bill): This should be null terminated - String abs_path = get_fullpath_core(a, file_str); - if (gb_file_exists(cast(char *)abs_path.text)) { - export_path = abs_path; - } - } - export_path = string_trim_whitespace(export_path); ed->fullpath = export_path; - try_add_import_path(p, export_path, file_str, ast_node_token(node).pos); + try_add_import_path(p, export_path, original_string, ast_node_token(node).pos); } else if (node->kind == AstNode_ForeignLibraryDecl) { ast_node(fl, ForeignLibraryDecl, node); + String file_str = fl->filepath.string; if (!is_import_path_valid(file_str)) { syntax_error(node, "Invalid `%.*s` path", LIT(fl->token.string)); diff --git a/src/string.cpp b/src/string.cpp index 397f41063..8e6006c46 100644 --- a/src/string.cpp +++ b/src/string.cpp @@ -16,11 +16,11 @@ struct String { isize len; u8 &operator[](isize i) { - GB_ASSERT(0 <= i && i < len); + GB_ASSERT_MSG(0 <= i && i < len, "[%td]", i); return text[i]; } u8 const &operator[](isize i) const { - GB_ASSERT(0 <= i && i < len); + GB_ASSERT_MSG(0 <= i && i < len, "[%td]", i); return text[i]; } }; @@ -38,11 +38,11 @@ struct String16 { wchar_t *text; isize len; wchar_t &operator[](isize i) { - GB_ASSERT(0 <= i && i < len); + GB_ASSERT_MSG(0 <= i && i < len, "[%td]", i); return text[i]; } wchar_t const &operator[](isize i) const { - GB_ASSERT(0 <= i && i < len); + GB_ASSERT_MSG(0 <= i && i < len, "[%td]", i); return text[i]; } }; @@ -284,6 +284,21 @@ String remove_directory_from_path(String s) { } +String concatenate_strings(gbAllocator a, String x, String y) { + isize len = x.len+y.len; + u8 *data = gb_alloc_array(a, u8, len+1); + gb_memmove(data, x.text, x.len); + gb_memmove(data+x.len, y.text, y.len); + data[len] = 0; + return make_string(data, len); +} + +String copy_string(gbAllocator a, String s) { + u8 *data = gb_alloc_array(a, u8, s.len+1); + gb_memmove(data, s.text, s.len); + data[s.len] = 0; + return make_string(data, s.len); +}