Browse Source

Library collections

Ginger Bill 8 years ago
parent
commit
8e3b77aba8

+ 1 - 1
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

+ 4 - 6
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;

+ 1 - 1
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
 
 

+ 6 - 6
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;

+ 1 - 1
core/hash.odin

@@ -1,4 +1,4 @@
-import "mem.odin";
+import "core:mem.odin";
 
 adler32 :: proc(data: []u8) -> u32 {
 	ADLER_CONST :: 65521;

+ 3 - 3
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" ---;

+ 3 - 3
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");
 

+ 3 - 3
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);

+ 1 - 1
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;

+ 2 - 2
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;

+ 1 - 1
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;

+ 1 - 1
core/strconv.odin

@@ -1,4 +1,4 @@
-using import "decimal.odin";
+using import "core:decimal.odin";
 
 Int_Flag :: enum {
 	Prefix = 1<<0,

+ 1 - 1
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);

+ 2 - 2
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";

+ 2 - 2
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;

+ 2 - 2
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;

+ 1 - 1
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;

+ 1 - 1
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;

+ 18 - 18
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

+ 0 - 0
shared/.gitkeep


+ 44 - 22
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<LibraryCollections> 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';
-
-	res = path_to_fullpath(a, make_string(str, str_len));
-	gb_free(heap_allocator(), str);
-	return res;
+	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;
+
+	String res = make_string(str, i);
+	res = string_trim_whitespace(res);
+	return path_to_fullpath(a, res);
 }
 
 

+ 28 - 0
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
+

+ 98 - 0
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<BuildFlag> *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<String> args) {
 	Array<BuildFlag> build_flags = {};
 	array_init(&build_flags, heap_allocator(), BuildFlag_COUNT);
@@ -211,6 +242,7 @@ bool parse_build_flags(Array<String> 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<String> flag_args = args;
@@ -358,6 +390,68 @@ bool parse_build_flags(Array<String> 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<String> args = setup_args(arg_count, arg_ptr);
 
 #if 1

+ 61 - 48
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<AstNode *> 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, Array<AstNod
 		    node->kind != 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));

+ 19 - 4
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);
+}