Browse Source

Merge branch 'odin-lang:master' into badaxis/Windows-Audio&Winmm

Vincent Billet 3 months ago
parent
commit
bb274ab512
49 changed files with 979 additions and 323 deletions
  1. 4 6
      core/container/priority_queue/priority_queue.odin
  2. 1 1
      core/crypto/_aes/hw_intel/api.odin
  3. 1 1
      core/crypto/_chacha20/simd128/chacha20_simd128.odin
  4. 1 1
      core/crypto/_chacha20/simd256/chacha20_simd256.odin
  5. 1 1
      core/crypto/sha2/sha2_impl_hw_intel.odin
  6. 53 11
      core/log/file_console_logger.odin
  7. 1 1
      core/math/big/radix.odin
  8. 1 1
      core/mem/tlsf/tlsf_internal.odin
  9. 18 0
      core/os/os2/path_linux.odin
  10. 17 0
      core/os/os2/path_posix.odin
  11. 0 21
      core/os/os2/path_posixfs.odin
  12. 12 12
      core/sys/freebsd/syscalls.odin
  13. 10 6
      core/sys/info/cpu_arm.odin
  14. 1 1
      core/sys/info/cpu_darwin_arm64.odin
  15. 16 12
      core/sys/info/cpu_intel.odin
  16. 1 1
      core/sys/info/cpu_linux_arm.odin
  17. 38 0
      core/sys/info/cpu_linux_intel.odin
  18. 2 2
      core/sys/info/cpu_linux_riscv64.odin
  19. 7 3
      core/sys/info/cpu_riscv64.odin
  20. 28 0
      core/sys/info/cpu_windows.odin
  21. 8 6
      core/sys/info/doc.odin
  22. 1 2
      core/sys/windows/kernel32.odin
  23. 0 0
      core/terminal/ansi/ansi.odin
  24. 0 0
      core/terminal/ansi/doc.odin
  25. 4 0
      core/terminal/doc.odin
  26. 87 0
      core/terminal/internal.odin
  27. 36 0
      core/terminal/terminal.odin
  28. 16 0
      core/terminal/terminal_posix.odin
  29. 60 0
      core/terminal/terminal_windows.odin
  30. 1 1
      core/testing/reporting.odin
  31. 94 70
      core/testing/runner.odin
  32. 0 36
      core/testing/runner_windows.odin
  33. 7 5
      core/testing/signal_handler_libc.odin
  34. 0 3
      core/text/regex/common/common.odin
  35. 9 5
      core/text/regex/compiler/compiler.odin
  36. 62 9
      core/text/regex/regex.odin
  37. 3 3
      core/text/regex/virtual_machine/virtual_machine.odin
  38. 4 2
      core/text/scanner/scanner.odin
  39. 5 2
      examples/all/all_main.odin
  40. 33 9
      src/build_settings.cpp
  41. 14 6
      src/check_expr.cpp
  42. 15 0
      src/linker.cpp
  43. 24 4
      src/llvm_backend.cpp
  44. 89 50
      src/main.cpp
  45. 6 4
      tests/benchmark/text/regex/benchmark_regex.odin
  46. 174 19
      tests/core/text/regex/test_core_text_regex.odin
  47. 1 1
      vendor/sdl2/sdl_rwops.odin
  48. 12 4
      vendor/sdl3/sdl3_mutex.odin
  49. 1 1
      vendor/wgpu/wgpu.js

+ 4 - 6
core/container/priority_queue/priority_queue.odin

@@ -133,12 +133,10 @@ pop_safe :: proc(pq: ^$Q/Priority_Queue($T), loc := #caller_location) -> (value:
 remove :: proc(pq: ^$Q/Priority_Queue($T), i: int) -> (value: T, ok: bool) {
 remove :: proc(pq: ^$Q/Priority_Queue($T), i: int) -> (value: T, ok: bool) {
 	n := builtin.len(pq.queue)
 	n := builtin.len(pq.queue)
 	if 0 <= i && i < n {
 	if 0 <= i && i < n {
-		if n != i {
-			pq.swap(pq.queue[:], i, n)
-			_shift_down(pq, i, n)
-			_shift_up(pq, i)
-		}
-		value, ok = builtin.pop_safe(&pq.queue)
+		pq.swap(pq.queue[:], i, n-1)
+		_shift_down(pq, i, n-1)
+		_shift_up(pq, i)
+		value, ok = builtin.pop(&pq.queue), true
 	}
 	}
 	return
 	return
 }
 }

+ 1 - 1
core/crypto/_aes/hw_intel/api.odin

@@ -6,7 +6,7 @@ import "core:sys/info"
 // is_supported returns true iff hardware accelerated AES
 // is_supported returns true iff hardware accelerated AES
 // is supported.
 // is supported.
 is_supported :: proc "contextless" () -> bool {
 is_supported :: proc "contextless" () -> bool {
-	features, ok := info.cpu_features.?
+	features, ok := info.cpu.features.?
 	if !ok {
 	if !ok {
 		return false
 		return false
 	}
 	}

+ 1 - 1
core/crypto/_chacha20/simd128/chacha20_simd128.odin

@@ -227,7 +227,7 @@ is_performant :: proc "contextless" () -> bool {
 			req_features :: info.CPU_Features{.V}
 			req_features :: info.CPU_Features{.V}
 		}
 		}
 
 
-		features, ok := info.cpu_features.?
+		features, ok := info.cpu.features.?
 		if !ok {
 		if !ok {
 			return false
 			return false
 		}
 		}

+ 1 - 1
core/crypto/_chacha20/simd256/chacha20_simd256.odin

@@ -41,7 +41,7 @@ _VEC_TWO: simd.u64x4 : {2, 0, 2, 0}
 is_performant :: proc "contextless" () -> bool {
 is_performant :: proc "contextless" () -> bool {
 	req_features :: info.CPU_Features{.avx, .avx2}
 	req_features :: info.CPU_Features{.avx, .avx2}
 
 
-	features, ok := info.cpu_features.?
+	features, ok := info.cpu.features.?
 	if !ok {
 	if !ok {
 		return false
 		return false
 	}
 	}

+ 1 - 1
core/crypto/sha2/sha2_impl_hw_intel.odin

@@ -52,7 +52,7 @@ K_15 :: simd.u64x2{0xa4506ceb90befffa, 0xc67178f2bef9a3f7}
 // is_hardware_accelerated_256 returns true iff hardware accelerated
 // is_hardware_accelerated_256 returns true iff hardware accelerated
 // SHA-224/SHA-256 is supported.
 // SHA-224/SHA-256 is supported.
 is_hardware_accelerated_256 :: proc "contextless" () -> bool {
 is_hardware_accelerated_256 :: proc "contextless" () -> bool {
-	features, ok := info.cpu_features.?
+	features, ok := info.cpu.features.?
 	if !ok {
 	if !ok {
 		return false
 		return false
 	}
 	}

+ 53 - 11
core/log/file_console_logger.odin

@@ -2,10 +2,12 @@
 #+build !orca
 #+build !orca
 package log
 package log
 
 
-import "core:encoding/ansi"
+import "base:runtime"
 import "core:fmt"
 import "core:fmt"
 import "core:strings"
 import "core:strings"
 import "core:os"
 import "core:os"
+import "core:terminal"
+import "core:terminal/ansi"
 import "core:time"
 import "core:time"
 
 
 Level_Headers := [?]string{
 Level_Headers := [?]string{
@@ -37,11 +39,36 @@ File_Console_Logger_Data :: struct {
 	ident: string,
 	ident: string,
 }
 }
 
 
+@(private) global_subtract_stdout_options: Options
+@(private) global_subtract_stderr_options: Options
+
+@(init, private)
+init_standard_stream_status :: proc() {
+	// NOTE(Feoramund): While it is technically possible for these streams to
+	// be redirected during the runtime of the program, the cost of checking on
+	// every single log message is not worth it to support such an
+	// uncommonly-used feature.
+	if terminal.color_enabled {
+		// This is done this way because it's possible that only one of these
+		// streams could be redirected to a file.
+		if !terminal.is_terminal(os.stdout) {
+			global_subtract_stdout_options = {.Terminal_Color}
+		}
+		if !terminal.is_terminal(os.stderr) {
+			global_subtract_stderr_options = {.Terminal_Color}
+		}
+	} else {
+		// Override any terminal coloring.
+		global_subtract_stdout_options = {.Terminal_Color}
+		global_subtract_stderr_options = {.Terminal_Color}
+	}
+}
+
 create_file_logger :: proc(h: os.Handle, lowest := Level.Debug, opt := Default_File_Logger_Opts, ident := "", allocator := context.allocator) -> Logger {
 create_file_logger :: proc(h: os.Handle, lowest := Level.Debug, opt := Default_File_Logger_Opts, ident := "", allocator := context.allocator) -> Logger {
 	data := new(File_Console_Logger_Data, allocator)
 	data := new(File_Console_Logger_Data, allocator)
 	data.file_handle = h
 	data.file_handle = h
 	data.ident = ident
 	data.ident = ident
-	return Logger{file_console_logger_proc, data, lowest, opt}
+	return Logger{file_logger_proc, data, lowest, opt}
 }
 }
 
 
 destroy_file_logger :: proc(log: Logger, allocator := context.allocator) {
 destroy_file_logger :: proc(log: Logger, allocator := context.allocator) {
@@ -56,19 +83,15 @@ create_console_logger :: proc(lowest := Level.Debug, opt := Default_Console_Logg
 	data := new(File_Console_Logger_Data, allocator)
 	data := new(File_Console_Logger_Data, allocator)
 	data.file_handle = os.INVALID_HANDLE
 	data.file_handle = os.INVALID_HANDLE
 	data.ident = ident
 	data.ident = ident
-	return Logger{file_console_logger_proc, data, lowest, opt}
+	return Logger{console_logger_proc, data, lowest, opt}
 }
 }
 
 
 destroy_console_logger :: proc(log: Logger, allocator := context.allocator) {
 destroy_console_logger :: proc(log: Logger, allocator := context.allocator) {
 	free(log.data, allocator)
 	free(log.data, allocator)
 }
 }
 
 
-file_console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) {
-	data := cast(^File_Console_Logger_Data)logger_data
-	h: os.Handle = os.stdout if level <= Level.Error else os.stderr
-	if data.file_handle != os.INVALID_HANDLE {
-		h = data.file_handle
-	}
+@(private)
+_file_console_logger_proc :: proc(h: os.Handle, ident: string, level: Level, text: string, options: Options, location: runtime.Source_Code_Location) {
 	backing: [1024]byte //NOTE(Hoej): 1024 might be too much for a header backing, unless somebody has really long paths.
 	backing: [1024]byte //NOTE(Hoej): 1024 might be too much for a header backing, unless somebody has really long paths.
 	buf := strings.builder_from_bytes(backing[:])
 	buf := strings.builder_from_bytes(backing[:])
 
 
@@ -86,13 +109,32 @@ file_console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string
 		fmt.sbprintf(&buf, "[{}] ", os.current_thread_id())
 		fmt.sbprintf(&buf, "[{}] ", os.current_thread_id())
 	}
 	}
 
 
-	if data.ident != "" {
-		fmt.sbprintf(&buf, "[%s] ", data.ident)
+	if ident != "" {
+		fmt.sbprintf(&buf, "[%s] ", ident)
 	}
 	}
 	//TODO(Hoej): When we have better atomics and such, make this thread-safe
 	//TODO(Hoej): When we have better atomics and such, make this thread-safe
 	fmt.fprintf(h, "%s%s\n", strings.to_string(buf), text)
 	fmt.fprintf(h, "%s%s\n", strings.to_string(buf), text)
 }
 }
 
 
+file_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) {
+	data := cast(^File_Console_Logger_Data)logger_data
+	_file_console_logger_proc(data.file_handle, data.ident, level, text, options, location)
+}
+
+console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) {
+	options := options
+	data := cast(^File_Console_Logger_Data)logger_data
+	h: os.Handle = ---
+	if level < Level.Error {
+		h = os.stdout
+		options -= global_subtract_stdout_options
+	} else {
+		h = os.stderr
+		options -= global_subtract_stderr_options
+	}
+	_file_console_logger_proc(h, data.ident, level, text, options, location)
+}
+
 do_level_header :: proc(opts: Options, str: ^strings.Builder, level: Level) {
 do_level_header :: proc(opts: Options, str: ^strings.Builder, level: Level) {
 
 
 	RESET     :: ansi.CSI + ansi.RESET           + ansi.SGR
 	RESET     :: ansi.CSI + ansi.RESET           + ansi.SGR

+ 1 - 1
core/math/big/radix.odin

@@ -280,7 +280,7 @@ int_atoi :: proc(res: ^Int, input: string, radix := i8(10), allocator := context
 		}
 		}
 
 
 		pos := ch - '+'
 		pos := ch - '+'
-		if RADIX_TABLE_REVERSE_SIZE <= pos {
+		if RADIX_TABLE_REVERSE_SIZE <= u32(pos) {
 			break
 			break
 		}
 		}
 		y := RADIX_TABLE_REVERSE[pos]
 		y := RADIX_TABLE_REVERSE[pos]

+ 1 - 1
core/mem/tlsf/tlsf_internal.odin

@@ -477,7 +477,7 @@ block_mark_as_free :: proc(block: ^Block_Header) {
 }
 }
 
 
 @(private, no_sanitize_address)
 @(private, no_sanitize_address)
-block_mark_as_used :: proc(block: ^Block_Header, ) {
+block_mark_as_used :: proc(block: ^Block_Header) {
 	next := block_next(block)
 	next := block_next(block)
 	block_set_prev_used(next)
 	block_set_prev_used(next)
 	block_set_used(block)
 	block_set_used(block)

+ 18 - 0
core/os/os2/path_linux.odin

@@ -207,3 +207,21 @@ _get_full_path :: proc(fd: linux.Fd, allocator: runtime.Allocator) -> (fullpath:
 	}
 	}
 	return
 	return
 }
 }
+
+_get_absolute_path :: proc(path: string, allocator: runtime.Allocator) -> (absolute_path: string, err: Error) {
+	rel := path
+	if rel == "" {
+		rel = "."
+	}
+
+	temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator })
+
+	fd, errno := linux.open(clone_to_cstring(path, temp_allocator) or_return, {})
+	if errno != nil {
+		err = _get_platform_error(errno)
+		return
+	}
+	defer linux.close(fd)
+
+	return _get_full_path(fd, allocator)
+}

+ 17 - 0
core/os/os2/path_posix.odin

@@ -123,3 +123,20 @@ _set_working_directory :: proc(dir: string) -> (err: Error) {
 	}
 	}
 	return
 	return
 }
 }
+
+_get_absolute_path :: proc(path: string, allocator: runtime.Allocator) -> (absolute_path: string, err: Error) {
+	rel := path
+	if rel == "" {
+		rel = "."
+	}
+	temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator })
+	rel_cstr := clone_to_cstring(rel, temp_allocator) or_return
+	path_ptr := posix.realpath(rel_cstr, nil)
+	if path_ptr == nil {
+		return "", Platform_Error(posix.errno())
+	}
+	defer posix.free(path_ptr)
+
+	path_str := clone_string(string(path_ptr), allocator) or_return
+	return path_str, nil
+}

+ 0 - 21
core/os/os2/path_posixfs.odin

@@ -4,10 +4,6 @@ package os2
 
 
 // This implementation is for all systems that have POSIX-compliant filesystem paths.
 // This implementation is for all systems that have POSIX-compliant filesystem paths.
 
 
-import "base:runtime"
-import "core:strings"
-import "core:sys/posix"
-
 _are_paths_identical :: proc(a, b: string) -> (identical: bool) {
 _are_paths_identical :: proc(a, b: string) -> (identical: bool) {
 	return a == b
 	return a == b
 }
 }
@@ -26,23 +22,6 @@ _is_absolute_path :: proc(path: string) -> bool {
 	return len(path) > 0 && _is_path_separator(path[0])
 	return len(path) > 0 && _is_path_separator(path[0])
 }
 }
 
 
-_get_absolute_path :: proc(path: string, allocator: runtime.Allocator) -> (absolute_path: string, err: Error) {
-	rel := path
-	if rel == "" {
-		rel = "."
-	}
-	temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator })
-	rel_cstr := strings.clone_to_cstring(rel, temp_allocator)
-	path_ptr := posix.realpath(rel_cstr, nil)
-	if path_ptr == nil {
-		return "", Platform_Error(posix.errno())
-	}
-	defer posix.free(path_ptr)
-
-	path_str := strings.clone(string(path_ptr), allocator)
-	return path_str, nil
-}
-
 _get_relative_path_handle_start :: proc(base, target: string) -> bool {
 _get_relative_path_handle_start :: proc(base, target: string) -> bool {
 	base_rooted   := len(base)   > 0 && _is_path_separator(base[0])
 	base_rooted   := len(base)   > 0 && _is_path_separator(base[0])
 	target_rooted := len(target) > 0 && _is_path_separator(target[0])
 	target_rooted := len(target) > 0 && _is_path_separator(target[0])

+ 12 - 12
core/sys/freebsd/syscalls.odin

@@ -204,21 +204,21 @@ accept_nil :: proc "contextless" (s: Fd) -> (Fd, Errno) {
 accept :: proc { accept_T, accept_nil }
 accept :: proc { accept_T, accept_nil }
 
 
 getsockname_or_peername :: proc "contextless" (s: Fd, sockaddr: ^$T, is_peer: bool) -> Errno {
 getsockname_or_peername :: proc "contextless" (s: Fd, sockaddr: ^$T, is_peer: bool) -> Errno {
-    // sockaddr must contain a valid pointer, or this will segfault because
-    // we're telling the syscall that there's memory available to write to.
-    addrlen: socklen_t = size_of(T)
+	// sockaddr must contain a valid pointer, or this will segfault because
+	// we're telling the syscall that there's memory available to write to.
+	addrlen: socklen_t = size_of(T)
 
 
-    result, ok := intrinsics.syscall_bsd(
-        is_peer ? SYS_getpeername : SYS_getsockname,
-        cast(uintptr)s,
-        cast(uintptr)sockaddr,
-        cast(uintptr)&addrlen)
+	result, ok := intrinsics.syscall_bsd(
+		is_peer ? SYS_getpeername : SYS_getsockname,
+		cast(uintptr)s,
+		cast(uintptr)sockaddr,
+		cast(uintptr)&addrlen)
 
 
-    if !ok {
-            return cast(Errno)result
-    }
+	if !ok {
+			return cast(Errno)result
+	}
 
 
-    return nil
+	return nil
 }
 }
 
 
 // Get name of connected peer
 // Get name of connected peer

+ 10 - 6
core/sys/info/cpu_arm.odin

@@ -40,9 +40,13 @@ CPU_Feature :: enum u64 {
 }
 }
 
 
 CPU_Features :: distinct bit_set[CPU_Feature; u64]
 CPU_Features :: distinct bit_set[CPU_Feature; u64]
-
-cpu_features: Maybe(CPU_Features)
-cpu_name: Maybe(string)
+CPU :: struct {
+	name:           Maybe(string),
+	features:       Maybe(CPU_Features),
+	physical_cores: int,
+	logical_cores:  int,
+}
+cpu: CPU
 
 
 @(private)
 @(private)
 cpu_name_buf: [128]byte
 cpu_name_buf: [128]byte
@@ -53,7 +57,7 @@ init_cpu_name :: proc "contextless" () {
 
 
 	when ODIN_OS == .Darwin {
 	when ODIN_OS == .Darwin {
 		if unix.sysctlbyname("machdep.cpu.brand_string", &cpu_name_buf) {
 		if unix.sysctlbyname("machdep.cpu.brand_string", &cpu_name_buf) {
-			cpu_name = string(cstring(rawptr(&cpu_name_buf)))
+			cpu.name = string(cstring(rawptr(&cpu_name_buf)))
 			generic = false
 			generic = false
 		}
 		}
 	}
 	}
@@ -61,10 +65,10 @@ init_cpu_name :: proc "contextless" () {
 	if generic {
 	if generic {
 		when ODIN_ARCH == .arm64 {
 		when ODIN_ARCH == .arm64 {
 			copy(cpu_name_buf[:], "ARM64")
 			copy(cpu_name_buf[:], "ARM64")
-			cpu_name = string(cpu_name_buf[:len("ARM64")])
+			cpu.name = string(cpu_name_buf[:len("ARM64")])
 		} else {
 		} else {
 			copy(cpu_name_buf[:], "ARM")
 			copy(cpu_name_buf[:], "ARM")
-			cpu_name = string(cpu_name_buf[:len("ARM")])
+			cpu.name = string(cpu_name_buf[:len("ARM")])
 		}
 		}
 	}
 	}
 }
 }

+ 1 - 1
core/sys/info/cpu_darwin_arm64.odin

@@ -5,7 +5,7 @@ import "core:sys/unix"
 @(init, private)
 @(init, private)
 init_cpu_features :: proc "contextless" () {
 init_cpu_features :: proc "contextless" () {
 	@(static) features: CPU_Features
 	@(static) features: CPU_Features
-	defer cpu_features = features
+	defer cpu.features = features
 
 
 	try_set :: proc "contextless" (name: cstring, feature: CPU_Feature) -> (ok: bool) {
 	try_set :: proc "contextless" (name: cstring, feature: CPU_Feature) -> (ok: bool) {
 		support: b32
 		support: b32

+ 16 - 12
core/sys/info/cpu_intel.odin

@@ -3,12 +3,6 @@ package sysinfo
 
 
 import "base:intrinsics"
 import "base:intrinsics"
 
 
-// cpuid :: proc(ax, cx: u32) -> (eax, ebc, ecx, edx: u32) ---
-cpuid :: intrinsics.x86_cpuid
-
-// xgetbv :: proc(cx: u32) -> (eax, edx: u32) ---
-xgetbv :: intrinsics.x86_xgetbv
-
 CPU_Feature :: enum u64 {
 CPU_Feature :: enum u64 {
 	aes,       // AES hardware implementation (AES NI)
 	aes,       // AES hardware implementation (AES NI)
 	adx,       // Multi-precision add-carry instruction extensions
 	adx,       // Multi-precision add-carry instruction extensions
@@ -49,9 +43,13 @@ CPU_Feature :: enum u64 {
 }
 }
 
 
 CPU_Features :: distinct bit_set[CPU_Feature; u64]
 CPU_Features :: distinct bit_set[CPU_Feature; u64]
-
-cpu_features: Maybe(CPU_Features)
-cpu_name:     Maybe(string)
+CPU :: struct {
+	name:           Maybe(string),
+	features:       Maybe(CPU_Features),
+	physical_cores: int, // Initialized by cpu_<os>.odin
+	logical_cores:  int, // Initialized by cpu_<os>.odin
+}
+cpu: CPU
 
 
 @(init, private)
 @(init, private)
 init_cpu_features :: proc "c" () {
 init_cpu_features :: proc "c" () {
@@ -88,7 +86,7 @@ init_cpu_features :: proc "c" () {
 	when ODIN_OS == .FreeBSD || ODIN_OS == .OpenBSD || ODIN_OS == .NetBSD {
 	when ODIN_OS == .FreeBSD || ODIN_OS == .OpenBSD || ODIN_OS == .NetBSD {
 		// xgetbv is an illegal instruction under FreeBSD 13, OpenBSD 7.1 and NetBSD 10
 		// xgetbv is an illegal instruction under FreeBSD 13, OpenBSD 7.1 and NetBSD 10
 		// return before probing further
 		// return before probing further
-		cpu_features = set
+		cpu.features = set
 		return
 		return
 	}
 	}
 
 
@@ -151,7 +149,7 @@ init_cpu_features :: proc "c" () {
 	try_set(&set, .rdseed, 18, ebx7)
 	try_set(&set, .rdseed, 18, ebx7)
 	try_set(&set, .adx,    19, ebx7)
 	try_set(&set, .adx,    19, ebx7)
 
 
-	cpu_features = set
+	cpu.features = set
 }
 }
 
 
 @(private)
 @(private)
@@ -179,5 +177,11 @@ init_cpu_name :: proc "c" () {
 	for len(brand) > 0 && brand[len(brand) - 1] == 0 || brand[len(brand) - 1] == ' ' {
 	for len(brand) > 0 && brand[len(brand) - 1] == 0 || brand[len(brand) - 1] == ' ' {
 		brand = brand[:len(brand) - 1]
 		brand = brand[:len(brand) - 1]
 	}
 	}
-	cpu_name = brand
+	cpu.name = brand
 }
 }
+
+// cpuid :: proc(ax, cx: u32) -> (eax, ebc, ecx, edx: u32) ---
+cpuid :: intrinsics.x86_cpuid
+
+// xgetbv :: proc(cx: u32) -> (eax, edx: u32) ---
+xgetbv :: intrinsics.x86_xgetbv

+ 1 - 1
core/sys/info/cpu_linux_arm.odin

@@ -17,7 +17,7 @@ init_cpu_features :: proc() {
 	if rerr != .NONE || n == 0 { return }
 	if rerr != .NONE || n == 0 { return }
 
 
 	features: CPU_Features
 	features: CPU_Features
-	defer cpu_features = features
+	defer cpu.features = features
 
 
 	str := string(buf[:n])
 	str := string(buf[:n])
 	for line in strings.split_lines_iterator(&str) {
 	for line in strings.split_lines_iterator(&str) {

+ 38 - 0
core/sys/info/cpu_linux_intel.odin

@@ -0,0 +1,38 @@
+#+build i386, amd64
+#+build linux
+package sysinfo
+
+import "core:sys/linux"
+import "core:strings"
+import "core:strconv"
+
+@(init, private)
+init_cpu_core_count :: proc() {
+	fd, err := linux.open("/proc/cpuinfo", {})
+	if err != .NONE { return }
+	defer linux.close(fd)
+
+	// This is probably enough right?
+	buf: [4096]byte
+	n, rerr := linux.read(fd, buf[:])
+	if rerr != .NONE || n == 0 { return }
+
+	str := string(buf[:n])
+	for line in strings.split_lines_iterator(&str) {
+		key, _, value := strings.partition(line, ":")
+		key   = strings.trim_space(key)
+		value = strings.trim_space(value)
+
+		if key == "cpu cores" {
+			if num_physical_cores, ok := strconv.parse_int(value); ok {
+				cpu.physical_cores = num_physical_cores
+			}
+		}
+
+		if key == "siblings" {
+			if num_logical_cores, ok := strconv.parse_int(value); ok {
+				cpu.logical_cores = num_logical_cores
+			}
+		}
+	}
+}

+ 2 - 2
core/sys/info/cpu_linux_riscv64.odin

@@ -9,7 +9,7 @@ import "core:sys/linux"
 @(init, private)
 @(init, private)
 init_cpu_features :: proc() {
 init_cpu_features :: proc() {
 	_features: CPU_Features
 	_features: CPU_Features
-	defer cpu_features = _features
+	defer cpu.features = _features
 
 
 	HWCAP_Bits :: enum u64 {
 	HWCAP_Bits :: enum u64 {
 		I = 'I' - 'A',
 		I = 'I' - 'A',
@@ -109,5 +109,5 @@ init_cpu_features :: proc() {
 
 
 @(init, private)
 @(init, private)
 init_cpu_name :: proc() {
 init_cpu_name :: proc() {
-	cpu_name = "RISCV64"
+	cpu.name = "RISCV64"
 }
 }

+ 7 - 3
core/sys/info/cpu_riscv64.odin

@@ -95,6 +95,10 @@ CPU_Feature :: enum u64 {
 }
 }
 
 
 CPU_Features :: distinct bit_set[CPU_Feature; u64]
 CPU_Features :: distinct bit_set[CPU_Feature; u64]
-
-cpu_features: Maybe(CPU_Features)
-cpu_name: Maybe(string)
+CPU :: struct {
+	name:           Maybe(string),
+	features:       Maybe(CPU_Features),
+	physical_cores: int,
+	logical_cores:  int,
+}
+cpu: CPU

+ 28 - 0
core/sys/info/cpu_windows.odin

@@ -0,0 +1,28 @@
+package sysinfo
+
+import sys "core:sys/windows"
+import "base:intrinsics"
+
+@(init, private)
+init_cpu_core_count :: proc() {
+	infos: []sys.SYSTEM_LOGICAL_PROCESSOR_INFORMATION
+	defer delete(infos)
+
+	returned_length: sys.DWORD
+	// Query for the required buffer size.
+	if ok := sys.GetLogicalProcessorInformation(raw_data(infos), &returned_length); !ok {
+		infos = make([]sys.SYSTEM_LOGICAL_PROCESSOR_INFORMATION, returned_length / size_of(sys.SYSTEM_LOGICAL_PROCESSOR_INFORMATION))
+	}
+
+	// If it still doesn't work, return
+	if ok := sys.GetLogicalProcessorInformation(raw_data(infos), &returned_length); !ok {
+		return
+	}
+
+	for info in infos {
+		#partial switch info.Relationship {
+		case .RelationProcessorCore: cpu.physical_cores += 1
+		case .RelationNumaNode:      cpu.logical_cores  += int(intrinsics.count_ones(info.ProcessorMask))
+		}
+	}
+}

+ 8 - 6
core/sys/info/doc.odin

@@ -26,13 +26,15 @@ Example:
 	import si "core:sys/info"
 	import si "core:sys/info"
 
 
 	main :: proc() {
 	main :: proc() {
-		fmt.printfln("Odin:  %v",    ODIN_VERSION)
-		fmt.printfln("OS:    %v",    si.os_version.as_string)
-		fmt.printfln("OS:    %#v",   si.os_version)
-		fmt.printfln("CPU:   %v",    si.cpu_name)
-		fmt.printfln("RAM:   %#.1M", si.ram.total_ram)
+		fmt.printfln("Odin:      %v",      ODIN_VERSION)
+		fmt.printfln("OS:        %v",      si.os_version.as_string)
+		fmt.printfln("OS:        %#v",     si.os_version)
+		fmt.printfln("CPU:       %v",      si.cpu.name)
+		fmt.printfln("CPU:       %v",      si.cpu.name)
+		fmt.printfln("CPU cores: %vc/%vt", si.cpu.physical_cores, si.cpu.logical_cores)
+		fmt.printfln("RAM:       %#.1M",   si.ram.total_ram)
 
 
-		// fmt.printfln("Features: %v",      si.cpu_features)
+		// fmt.printfln("Features: %v",      si.cpu.features)
 		// fmt.printfln("MacOS version: %v", si.macos_version)
 		// fmt.printfln("MacOS version: %v", si.macos_version)
 
 
 		fmt.println()
 		fmt.println()

+ 1 - 2
core/sys/windows/kernel32.odin

@@ -857,7 +857,6 @@ MEMORY_RESOURCE_NOTIFICATION_TYPE :: enum c_int {
 LowMemoryResourceNotification  :: MEMORY_RESOURCE_NOTIFICATION_TYPE.LowMemoryResourceNotification
 LowMemoryResourceNotification  :: MEMORY_RESOURCE_NOTIFICATION_TYPE.LowMemoryResourceNotification
 HighMemoryResourceNotification :: MEMORY_RESOURCE_NOTIFICATION_TYPE.HighMemoryResourceNotification
 HighMemoryResourceNotification :: MEMORY_RESOURCE_NOTIFICATION_TYPE.HighMemoryResourceNotification
 
 
-
 @(default_calling_convention="system")
 @(default_calling_convention="system")
 foreign kernel32 {
 foreign kernel32 {
 	CreateMemoryResourceNotification :: proc(
 	CreateMemoryResourceNotification :: proc(
@@ -1194,7 +1193,7 @@ DUMMYUNIONNAME_u :: struct #raw_union {
 SYSTEM_LOGICAL_PROCESSOR_INFORMATION :: struct {
 SYSTEM_LOGICAL_PROCESSOR_INFORMATION :: struct {
 	ProcessorMask: ULONG_PTR,
 	ProcessorMask: ULONG_PTR,
 	Relationship: LOGICAL_PROCESSOR_RELATIONSHIP,
 	Relationship: LOGICAL_PROCESSOR_RELATIONSHIP,
-	DummyUnion: DUMMYUNIONNAME_u,
+	using DummyUnion: DUMMYUNIONNAME_u,
 }
 }
 
 
 SYSTEM_POWER_STATUS :: struct {
 SYSTEM_POWER_STATUS :: struct {

+ 0 - 0
core/encoding/ansi/ansi.odin → core/terminal/ansi/ansi.odin


+ 0 - 0
core/encoding/ansi/doc.odin → core/terminal/ansi/doc.odin


+ 4 - 0
core/terminal/doc.odin

@@ -0,0 +1,4 @@
+/*
+This package is for interacting with the command line interface of the system.
+*/
+package terminal

+ 87 - 0
core/terminal/internal.odin

@@ -0,0 +1,87 @@
+#+private
+package terminal
+
+import "core:os"
+import "core:strings"
+
+// Reference documentation:
+//
+// - [[ https://no-color.org/ ]]
+// - [[ https://github.com/termstandard/colors ]]
+// - [[ https://invisible-island.net/ncurses/terminfo.src.html ]]
+
+get_no_color :: proc() -> bool {
+	if no_color, ok := os.lookup_env("NO_COLOR"); ok {
+		defer delete(no_color)
+		return no_color != ""
+	}
+	return false
+}
+
+get_environment_color :: proc() -> Color_Depth {
+	// `COLORTERM` is non-standard but widespread and unambiguous.
+	if colorterm, ok := os.lookup_env("COLORTERM"); ok {
+		defer delete(colorterm)
+		// These are the only values that are typically advertised that have
+		// anything to do with color depth.
+		if colorterm == "truecolor" || colorterm == "24bit" {
+			return .True_Color
+		}
+	}
+
+	if term, ok := os.lookup_env("TERM"); ok {
+		defer delete(term)
+		if strings.contains(term, "-truecolor") {
+			return .True_Color
+		}
+		if strings.contains(term, "-256color") {
+			return .Eight_Bit
+		}
+		if strings.contains(term, "-16color") {
+			return .Four_Bit
+		}
+
+		// The `terminfo` database, which is stored in binary on *nix
+		// platforms, has an undocumented format that is not guaranteed to be
+		// portable, so beyond this point, we can only make safe assumptions.
+		//
+		// This section should only be necessary for terminals that do not
+		// define any of the previous environment values.
+		//
+		// Only a small sampling of some common values are checked here.
+		switch term {
+		case "ansi":       fallthrough
+		case "konsole":    fallthrough
+		case "putty":      fallthrough
+		case "rxvt":       fallthrough
+		case "rxvt-color": fallthrough
+		case "screen":     fallthrough
+		case "st":         fallthrough
+		case "tmux":       fallthrough
+		case "vte":        fallthrough
+		case "xterm":      fallthrough
+		case "xterm-color":
+			return .Three_Bit
+		}
+	}
+
+	return .None
+}
+
+@(init)
+init_terminal :: proc() {
+	_init_terminal()
+
+	// We respect `NO_COLOR` specifically as a color-disabler but not as a
+	// blanket ban on any terminal manipulation codes, hence why this comes
+	// after `_init_terminal` which will allow Windows to enable Virtual
+	// Terminal Processing for non-color control sequences.
+	if !get_no_color() {
+		color_enabled = color_depth > .None
+	}
+}
+
+@(fini)
+fini_terminal :: proc() {
+	_fini_terminal()
+}

+ 36 - 0
core/terminal/terminal.odin

@@ -0,0 +1,36 @@
+package terminal
+
+import "core:os"
+
+/*
+This describes the range of colors that a terminal is capable of supporting.
+*/
+Color_Depth :: enum {
+	None,       // No color support
+	Three_Bit,  // 8 colors
+	Four_Bit,   // 16 colors
+	Eight_Bit,  // 256 colors
+	True_Color, // 24-bit true color
+}
+
+/*
+Returns true if the file `handle` is attached to a terminal.
+
+This is normally true for `os.stdout` and `os.stderr` unless they are
+redirected to a file.
+*/
+@(require_results)
+is_terminal :: proc(handle: os.Handle) -> bool {
+	return _is_terminal(handle)
+}
+
+/*
+This is true if the terminal is accepting any form of colored text output.
+*/
+color_enabled: bool
+
+/*
+This value reports the color depth support as reported by the terminal at the
+start of the program.
+*/
+color_depth: Color_Depth

+ 16 - 0
core/terminal/terminal_posix.odin

@@ -0,0 +1,16 @@
+#+private
+#+build linux, darwin, netbsd, openbsd, freebsd, haiku
+package terminal
+
+import "core:os"
+import "core:sys/posix"
+
+_is_terminal :: proc(handle: os.Handle) -> bool {
+	return bool(posix.isatty(posix.FD(handle)))
+}
+
+_init_terminal :: proc() {
+	color_depth = get_environment_color()
+}
+
+_fini_terminal :: proc() { }

+ 60 - 0
core/terminal/terminal_windows.odin

@@ -0,0 +1,60 @@
+#+private
+package terminal
+
+import "core:os"
+import "core:sys/windows"
+
+_is_terminal :: proc(handle: os.Handle) -> bool {
+	is_tty := windows.GetFileType(windows.HANDLE(handle)) == windows.FILE_TYPE_CHAR
+	return is_tty
+}
+
+old_modes: [2]struct{
+	handle: windows.DWORD,
+	mode: windows.DWORD,
+} = {
+	{windows.STD_OUTPUT_HANDLE, 0},
+	{windows.STD_ERROR_HANDLE, 0},
+}
+
+@(init)
+_init_terminal :: proc() {
+	vtp_enabled: bool
+
+	for &v in old_modes {
+		handle := windows.GetStdHandle(v.handle)
+		if handle == windows.INVALID_HANDLE || handle == nil {
+			return
+		}
+		if windows.GetConsoleMode(handle, &v.mode) {
+			windows.SetConsoleMode(handle, v.mode | windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+
+			new_mode: windows.DWORD
+			windows.GetConsoleMode(handle, &new_mode)
+
+			if new_mode & (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0 {
+				vtp_enabled = true
+			}
+		}
+	}
+
+	if vtp_enabled {
+		// This color depth is available on Windows 10 since build 10586.
+		color_depth = .Four_Bit
+	} else {
+		// The user may be on a non-default terminal emulator.
+		color_depth = get_environment_color()
+	}
+}
+
+@(fini)
+_fini_terminal :: proc() {
+	for v in old_modes {
+		handle := windows.GetStdHandle(v.handle)
+		if handle == windows.INVALID_HANDLE || handle == nil {
+			return
+		}
+		
+		windows.SetConsoleMode(handle, v.mode)
+	}
+}

+ 1 - 1
core/testing/reporting.odin

@@ -10,12 +10,12 @@ package testing
 */
 */
 
 
 import "base:runtime"
 import "base:runtime"
-import "core:encoding/ansi"
 import "core:fmt"
 import "core:fmt"
 import "core:io"
 import "core:io"
 import "core:mem"
 import "core:mem"
 import "core:path/filepath"
 import "core:path/filepath"
 import "core:strings"
 import "core:strings"
+import "core:terminal/ansi"
 
 
 // Definitions of colors for use in the test runner.
 // Definitions of colors for use in the test runner.
 SGR_RESET   :: ansi.CSI + ansi.RESET           + ansi.SGR
 SGR_RESET   :: ansi.CSI + ansi.RESET           + ansi.SGR

+ 94 - 70
core/testing/runner.odin

@@ -13,7 +13,6 @@ package testing
 import "base:intrinsics"
 import "base:intrinsics"
 import "base:runtime"
 import "base:runtime"
 import "core:bytes"
 import "core:bytes"
-import "core:encoding/ansi"
 @require import "core:encoding/base64"
 @require import "core:encoding/base64"
 @require import "core:encoding/json"
 @require import "core:encoding/json"
 import "core:fmt"
 import "core:fmt"
@@ -25,6 +24,8 @@ import "core:os"
 import "core:slice"
 import "core:slice"
 @require import "core:strings"
 @require import "core:strings"
 import "core:sync/chan"
 import "core:sync/chan"
+import "core:terminal"
+import "core:terminal/ansi"
 import "core:thread"
 import "core:thread"
 import "core:time"
 import "core:time"
 
 
@@ -44,6 +45,7 @@ PER_THREAD_MEMORY     : int    : #config(ODIN_TEST_THREAD_MEMORY, mem.ROLLBACK_S
 // The format is: `package.test_name,test_name_only,...`
 // The format is: `package.test_name,test_name_only,...`
 TEST_NAMES            : string : #config(ODIN_TEST_NAMES, "")
 TEST_NAMES            : string : #config(ODIN_TEST_NAMES, "")
 // Show the fancy animated progress report.
 // Show the fancy animated progress report.
+// This requires terminal color support, as well as STDOUT to not be redirected to a file.
 FANCY_OUTPUT          : bool   : #config(ODIN_TEST_FANCY, true)
 FANCY_OUTPUT          : bool   : #config(ODIN_TEST_FANCY, true)
 // Copy failed tests to the clipboard when done.
 // Copy failed tests to the clipboard when done.
 USE_CLIPBOARD         : bool   : #config(ODIN_TEST_CLIPBOARD, false)
 USE_CLIPBOARD         : bool   : #config(ODIN_TEST_CLIPBOARD, false)
@@ -70,6 +72,9 @@ get_log_level :: #force_inline proc() -> runtime.Logger_Level {
 	}
 	}
 }
 }
 
 
+@(private) global_log_colors_disabled: bool
+@(private) global_ansi_disabled: bool
+
 JSON :: struct {
 JSON :: struct {
 	total:    int,
 	total:    int,
 	success:  int,
 	success:  int,
@@ -129,11 +134,16 @@ run_test_task :: proc(task: thread.Task) {
 	
 	
 	context.assertion_failure_proc = test_assertion_failure_proc
 	context.assertion_failure_proc = test_assertion_failure_proc
 
 
+	logger_options := Default_Test_Logger_Opts
+	if global_log_colors_disabled {
+		logger_options -= {.Terminal_Color}
+	}
+
 	context.logger = {
 	context.logger = {
 		procedure = test_logger_proc,
 		procedure = test_logger_proc,
 		data = &data.t,
 		data = &data.t,
 		lowest_level = get_log_level(),
 		lowest_level = get_log_level(),
-		options = Default_Test_Logger_Opts,
+		options = logger_options,
 	}
 	}
 
 
 	random_generator_state: runtime.Default_Random_State
 	random_generator_state: runtime.Default_Random_State
@@ -204,13 +214,16 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 		}
 		}
 	}
 	}
 
 
-	when ODIN_OS == .Windows {
-		console_ansi_init()
-	}
-
 	stdout := io.to_writer(os.stream_from_handle(os.stdout))
 	stdout := io.to_writer(os.stream_from_handle(os.stdout))
 	stderr := io.to_writer(os.stream_from_handle(os.stderr))
 	stderr := io.to_writer(os.stream_from_handle(os.stderr))
 
 
+	// The animations are only ever shown through STDOUT;
+	// STDERR is used exclusively for logging regardless of error level.
+	global_log_colors_disabled = !terminal.color_enabled || !terminal.is_terminal(os.stderr)
+	global_ansi_disabled       = !terminal.is_terminal(os.stdout)
+
+	should_show_animations := FANCY_OUTPUT && terminal.color_enabled && !global_ansi_disabled
+
 	// -- Prepare test data.
 	// -- Prepare test data.
 
 
 	alloc_error: mem.Allocator_Error
 	alloc_error: mem.Allocator_Error
@@ -268,12 +281,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	total_done_count    := 0
 	total_done_count    := 0
 	total_test_count    := len(internal_tests)
 	total_test_count    := len(internal_tests)
 
 
-	when !FANCY_OUTPUT {
-		// This is strictly for updating the window title when the progress
-		// report is disabled. We're otherwise able to depend on the call to
-		// `needs_to_redraw`.
-		last_done_count := -1
-	}
+
+	// This is strictly for updating the window title when the progress
+	// report is disabled. We're otherwise able to depend on the call to
+	// `needs_to_redraw`.
+	last_done_count := -1
+
 
 
 	if total_test_count == 0 {
 	if total_test_count == 0 {
 		// Exit early.
 		// Exit early.
@@ -342,31 +355,31 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	fmt.assertf(alloc_error == nil, "Error allocating memory for test report: %v", alloc_error)
 	fmt.assertf(alloc_error == nil, "Error allocating memory for test report: %v", alloc_error)
 	defer destroy_report(&report)
 	defer destroy_report(&report)
 
 
-	when FANCY_OUTPUT {
-		// We cannot make use of the ANSI save/restore cursor codes, because they
-		// work by absolute screen coordinates. This will cause unnecessary
-		// scrollback if we print at the bottom of someone's terminal.
-		ansi_redraw_string := fmt.aprintf(
-			// ANSI for "go up N lines then erase the screen from the cursor forward."
-			ansi.CSI + "%i" + ansi.CPL + ansi.CSI + ansi.ED +
-			// We'll combine this with the window title format string, since it
-			// can be printed at the same time.
-			"%s",
-			// 1 extra line for the status bar.
-			1 + len(report.packages), OSC_WINDOW_TITLE)
-		assert(len(ansi_redraw_string) > 0, "Error allocating ANSI redraw string.")
-		defer delete(ansi_redraw_string)
-
-		thread_count_status_string: string = ---
-		{
-			PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH
 
 
-			unpadded := fmt.tprintf("%i thread%s", thread_count, "" if thread_count == 1 else "s")
-			thread_count_status_string = fmt.aprintf("%- *[1]s", unpadded, report.pkg_column_len + PADDING)
-			assert(len(thread_count_status_string) > 0, "Error allocating thread count status string.")
-		}
-		defer delete(thread_count_status_string)
+	// We cannot make use of the ANSI save/restore cursor codes, because they
+	// work by absolute screen coordinates. This will cause unnecessary
+	// scrollback if we print at the bottom of someone's terminal.
+	ansi_redraw_string := fmt.aprintf(
+		// ANSI for "go up N lines then erase the screen from the cursor forward."
+		ansi.CSI + "%i" + ansi.CPL + ansi.CSI + ansi.ED +
+		// We'll combine this with the window title format string, since it
+		// can be printed at the same time.
+		"%s",
+		// 1 extra line for the status bar.
+		1 + len(report.packages), OSC_WINDOW_TITLE)
+	assert(len(ansi_redraw_string) > 0, "Error allocating ANSI redraw string.")
+	defer delete(ansi_redraw_string)
+
+	thread_count_status_string: string = ---
+	{
+		PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH
+
+		unpadded := fmt.tprintf("%i thread%s", thread_count, "" if thread_count == 1 else "s")
+		thread_count_status_string = fmt.aprintf("%- *[1]s", unpadded, report.pkg_column_len + PADDING)
+		assert(len(thread_count_status_string) > 0, "Error allocating thread count status string.")
 	}
 	}
+	defer delete(thread_count_status_string)
+
 
 
 	task_data_slots: []Task_Data = ---
 	task_data_slots: []Task_Data = ---
 	task_data_slots, alloc_error = make([]Task_Data, thread_count)
 	task_data_slots, alloc_error = make([]Task_Data, thread_count)
@@ -442,11 +455,16 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	// digging through the source to divine everywhere it is used for that.
 	// digging through the source to divine everywhere it is used for that.
 	shared_log_allocator := context.allocator
 	shared_log_allocator := context.allocator
 
 
+	logger_options := Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure}
+	if global_log_colors_disabled {
+		logger_options -= {.Terminal_Color}
+	}
+
 	context.logger = {
 	context.logger = {
 		procedure = runner_logger_proc,
 		procedure = runner_logger_proc,
 		data = &log_messages,
 		data = &log_messages,
 		lowest_level = get_log_level(),
 		lowest_level = get_log_level(),
-		options = Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure},
+		options = logger_options,
 	}
 	}
 
 
 	run_index: int
 	run_index: int
@@ -481,11 +499,13 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 
 
 	setup_signal_handler()
 	setup_signal_handler()
 
 
-	fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE)
+	if !global_ansi_disabled {
+		fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE)
+	}
 
 
-	when FANCY_OUTPUT {
-		signals_were_raised := false
+	signals_were_raised := false
 
 
+	if should_show_animations {
 		redraw_report(stdout, report)
 		redraw_report(stdout, report)
 		draw_status_bar(stdout, thread_count_status_string, total_done_count, total_test_count)
 		draw_status_bar(stdout, thread_count_status_string, total_done_count, total_test_count)
 	}
 	}
@@ -703,22 +723,22 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 			break main_loop
 			break main_loop
 		}
 		}
 
 
-		when FANCY_OUTPUT {
-			// Because the bounds checking procs send directly to STDERR with
-			// no way to redirect or handle them, we need to at least try to
-			// let the user see those messages when using the animated progress
-			// report. This flag may be set by the block of code below if a
-			// signal is raised.
-			//
-			// It'll be purely by luck if the output is interleaved properly,
-			// given the nature of non-thread-safe printing.
-			//
-			// At worst, if Odin did not print any error for this signal, we'll
-			// just re-display the progress report. The fatal log error message
-			// should be enough to clue the user in that something dire has
-			// occurred.
-			bypass_progress_overwrite := false
-		}
+
+		// Because the bounds checking procs send directly to STDERR with
+		// no way to redirect or handle them, we need to at least try to
+		// let the user see those messages when using the animated progress
+		// report. This flag may be set by the block of code below if a
+		// signal is raised.
+		//
+		// It'll be purely by luck if the output is interleaved properly,
+		// given the nature of non-thread-safe printing.
+		//
+		// At worst, if Odin did not print any error for this signal, we'll
+		// just re-display the progress report. The fatal log error message
+		// should be enough to clue the user in that something dire has
+		// occurred.
+		bypass_progress_overwrite := false
+
 
 
 		if test_index, reason, ok := should_stop_test(); ok {
 		if test_index, reason, ok := should_stop_test(); ok {
 			#no_bounds_check report.all_test_states[test_index] = .Failed
 			#no_bounds_check report.all_test_states[test_index] = .Failed
@@ -752,7 +772,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 					log.fatalf("Caught signal to stop test #%i %s.%s for: %v.", test_index, it.pkg, it.name, reason)
 					log.fatalf("Caught signal to stop test #%i %s.%s for: %v.", test_index, it.pkg, it.name, reason)
 				}
 				}
 
 
-				when FANCY_OUTPUT {
+				if should_show_animations {
 					bypass_progress_overwrite = true
 					bypass_progress_overwrite = true
 					signals_were_raised = true
 					signals_were_raised = true
 				}
 				}
@@ -766,7 +786,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 
 
 		// -- Redraw.
 		// -- Redraw.
 
 
-		when FANCY_OUTPUT {
+		if should_show_animations {
 			if len(log_messages) == 0 && !needs_to_redraw(report) {
 			if len(log_messages) == 0 && !needs_to_redraw(report) {
 				continue main_loop
 				continue main_loop
 			}
 			}
@@ -776,7 +796,9 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 			}
 			}
 		} else {
 		} else {
 			if total_done_count != last_done_count {
 			if total_done_count != last_done_count {
-				fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count)
+				if !global_ansi_disabled {
+					fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count)
+				}
 				last_done_count = total_done_count
 				last_done_count = total_done_count
 			}
 			}
 
 
@@ -801,7 +823,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 		clear(&log_messages)
 		clear(&log_messages)
 		bytes.buffer_reset(&batch_buffer)
 		bytes.buffer_reset(&batch_buffer)
 
 
-		when FANCY_OUTPUT {
+		if should_show_animations {
 			redraw_report(batch_writer, report)
 			redraw_report(batch_writer, report)
 			draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count)
 			draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count)
 			fmt.wprint(stdout, bytes.buffer_to_string(&batch_buffer))
 			fmt.wprint(stdout, bytes.buffer_to_string(&batch_buffer))
@@ -822,7 +844,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 
 
 	finished_in := time.since(start_time)
 	finished_in := time.since(start_time)
 
 
-	when !FANCY_OUTPUT {
+	if !should_show_animations || !terminal.is_terminal(os.stderr) {
 		// One line to space out the results, since we don't have the status
 		// One line to space out the results, since we don't have the status
 		// bar in plain mode.
 		// bar in plain mode.
 		fmt.wprintln(batch_writer)
 		fmt.wprintln(batch_writer)
@@ -836,24 +858,28 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	
 	
 	if total_done_count != total_test_count {
 	if total_done_count != total_test_count {
 		not_run_count := total_test_count - total_done_count
 		not_run_count := total_test_count - total_done_count
+		message := " %i %s left undone." if global_log_colors_disabled else " " + SGR_READY + "%i" + SGR_RESET + " %s left undone."
 		fmt.wprintf(batch_writer,
 		fmt.wprintf(batch_writer,
-			" " + SGR_READY + "%i" + SGR_RESET + " %s left undone.",
+			message,
 			not_run_count,
 			not_run_count,
 			"test was" if not_run_count == 1 else "tests were")
 			"test was" if not_run_count == 1 else "tests were")
 	}
 	}
 
 
 	if total_success_count == total_test_count {
 	if total_success_count == total_test_count {
+		message := " %s successful." if global_log_colors_disabled else " %s " + SGR_SUCCESS + "successful." + SGR_RESET
 		fmt.wprintfln(batch_writer,
 		fmt.wprintfln(batch_writer,
-			" %s " + SGR_SUCCESS + "successful." + SGR_RESET,
+			message,
 			"The test was" if total_test_count == 1 else "All tests were")
 			"The test was" if total_test_count == 1 else "All tests were")
 	} else if total_failure_count > 0 {
 	} else if total_failure_count > 0 {
 		if total_failure_count == total_test_count {
 		if total_failure_count == total_test_count {
+			message := " %s failed." if global_log_colors_disabled else " %s " + SGR_FAILED + "failed." + SGR_RESET
 			fmt.wprintfln(batch_writer,
 			fmt.wprintfln(batch_writer,
-				" %s " + SGR_FAILED + "failed." + SGR_RESET,
+				message,
 				"The test" if total_test_count == 1 else "All tests")
 				"The test" if total_test_count == 1 else "All tests")
 		} else {
 		} else {
+			message := " %i test%s failed." if global_log_colors_disabled else " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed."
 			fmt.wprintfln(batch_writer,
 			fmt.wprintfln(batch_writer,
-				" " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed.",
+				message,
 				total_failure_count,
 				total_failure_count,
 				"" if total_failure_count == 1 else "s")
 				"" if total_failure_count == 1 else "s")
 		}
 		}
@@ -907,9 +933,11 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 		}
 		}
 	}
 	}
 
 
-	fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW)
+	if !global_ansi_disabled {
+		fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW)
+	}
 
 
-	when FANCY_OUTPUT {
+	if should_show_animations {
 		if signals_were_raised {
 		if signals_were_raised {
 			fmt.wprintln(batch_writer, `
 			fmt.wprintln(batch_writer, `
 Signals were raised during this test run. Log messages are likely to have collided with each other.
 Signals were raised during this test run. Log messages are likely to have collided with each other.
@@ -949,9 +977,5 @@ To partly mitigate this, redirect STDERR to a file or use the -define:ODIN_TEST_
 		fmt.assertf(err == nil, "Error writing JSON report: %v", err)
 		fmt.assertf(err == nil, "Error writing JSON report: %v", err)
 	}
 	}
 
 
-	when ODIN_OS == .Windows {
-		console_ansi_fini()
-	}
-
 	return total_success_count == total_test_count
 	return total_success_count == total_test_count
 }
 }

+ 0 - 36
core/testing/runner_windows.odin

@@ -1,36 +0,0 @@
-#+private
-package testing
-
-import win32 "core:sys/windows"
-
-old_stdout_mode: u32
-old_stderr_mode: u32
-
-console_ansi_init :: proc() {
-	stdout := win32.GetStdHandle(win32.STD_OUTPUT_HANDLE)
-	if stdout != win32.INVALID_HANDLE && stdout != nil {
-		if win32.GetConsoleMode(stdout, &old_stdout_mode) {
-			win32.SetConsoleMode(stdout, old_stdout_mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
-		}
-	}
-
-	stderr := win32.GetStdHandle(win32.STD_ERROR_HANDLE)
-	if stderr != win32.INVALID_HANDLE && stderr != nil {
-		if win32.GetConsoleMode(stderr, &old_stderr_mode) {
-			win32.SetConsoleMode(stderr, old_stderr_mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
-		}
-	}
-}
-
-// Restore the cursor on exit
-console_ansi_fini :: proc() {
-	stdout := win32.GetStdHandle(win32.STD_OUTPUT_HANDLE)
-	if stdout != win32.INVALID_HANDLE && stdout != nil {
-		win32.SetConsoleMode(stdout, old_stdout_mode)
-	}
-
-	stderr := win32.GetStdHandle(win32.STD_ERROR_HANDLE)
-	if stderr != win32.INVALID_HANDLE && stderr != nil {
-		win32.SetConsoleMode(stderr, old_stderr_mode)
-	}
-}

+ 7 - 5
core/testing/signal_handler_libc.odin

@@ -12,9 +12,9 @@ package testing
 
 
 import "base:intrinsics"
 import "base:intrinsics"
 import "core:c/libc"
 import "core:c/libc"
-import "core:encoding/ansi"
-import "core:sync"
 import "core:os"
 import "core:os"
+import "core:sync"
+import "core:terminal/ansi"
 
 
 @(private="file") stop_runner_flag: libc.sig_atomic_t
 @(private="file") stop_runner_flag: libc.sig_atomic_t
 
 
@@ -63,9 +63,11 @@ stop_test_callback :: proc "c" (sig: libc.int) {
 		// NOTE(Feoramund): Using these write calls in a signal handler is
 		// NOTE(Feoramund): Using these write calls in a signal handler is
 		// undefined behavior in C99 but possibly tolerated in POSIX 2008.
 		// undefined behavior in C99 but possibly tolerated in POSIX 2008.
 		// Either way, we may as well try to salvage what we can.
 		// Either way, we may as well try to salvage what we can.
-		show_cursor := ansi.CSI + ansi.DECTCEM_SHOW
-		libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout)
-		libc.fflush(libc.stdout)
+		if !global_ansi_disabled {
+			show_cursor := ansi.CSI + ansi.DECTCEM_SHOW
+			libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout)
+			libc.fflush(libc.stdout)
+		}
 
 
 		// This is an attempt at being compliant by avoiding printf.
 		// This is an attempt at being compliant by avoiding printf.
 		sigbuf: [8]byte
 		sigbuf: [8]byte

+ 0 - 3
core/text/regex/common/common.odin

@@ -15,8 +15,6 @@ MAX_PROGRAM_SIZE   :: int(max(i16))
 MAX_CLASSES        :: int(max(u8))
 MAX_CLASSES        :: int(max(u8))
 
 
 Flag :: enum u8 {
 Flag :: enum u8 {
-	// Global: try to match the pattern anywhere in the string.
-	Global,
 	// Multiline: treat `^` and `$` as if they also match newlines.
 	// Multiline: treat `^` and `$` as if they also match newlines.
 	Multiline,
 	Multiline,
 	// Case Insensitive: treat `a-z` as if it was also `A-Z`.
 	// Case Insensitive: treat `a-z` as if it was also `A-Z`.
@@ -36,7 +34,6 @@ Flags :: bit_set[Flag; u8]
 
 
 @(rodata)
 @(rodata)
 Flag_To_Letter := #sparse[Flag]u8 {
 Flag_To_Letter := #sparse[Flag]u8 {
-	.Global            = 'g',
 	.Multiline         = 'm',
 	.Multiline         = 'm',
 	.Case_Insensitive  = 'i',
 	.Case_Insensitive  = 'i',
 	.Ignore_Whitespace = 'x',
 	.Ignore_Whitespace = 'x',

+ 9 - 5
core/text/regex/compiler/compiler.odin

@@ -401,7 +401,7 @@ compile :: proc(tree: Node, flags: common.Flags) -> (code: Program, class_data:
 
 
 	pc_open := 0
 	pc_open := 0
 
 
-	add_global: if .Global in flags {
+	optimize_opening: {
 		// Check if the opening to the pattern is predictable.
 		// Check if the opening to the pattern is predictable.
 		// If so, use one of the optimized Wait opcodes.
 		// If so, use one of the optimized Wait opcodes.
 		iter := virtual_machine.Opcode_Iterator{ code[:], 0 }
 		iter := virtual_machine.Opcode_Iterator{ code[:], 0 }
@@ -412,7 +412,7 @@ compile :: proc(tree: Node, flags: common.Flags) -> (code: Program, class_data:
 				pc_open += size_of(Opcode)
 				pc_open += size_of(Opcode)
 				inject_at(&code, pc_open, Opcode(code[pc + size_of(Opcode) + pc_open]))
 				inject_at(&code, pc_open, Opcode(code[pc + size_of(Opcode) + pc_open]))
 				pc_open += size_of(u8)
 				pc_open += size_of(u8)
-				break add_global
+				break optimize_opening
 
 
 			case .Rune:
 			case .Rune:
 				operand := intrinsics.unaligned_load(cast(^rune)&code[pc+1])
 				operand := intrinsics.unaligned_load(cast(^rune)&code[pc+1])
@@ -420,24 +420,28 @@ compile :: proc(tree: Node, flags: common.Flags) -> (code: Program, class_data:
 				pc_open += size_of(Opcode)
 				pc_open += size_of(Opcode)
 				inject_raw(&code, pc_open, operand)
 				inject_raw(&code, pc_open, operand)
 				pc_open += size_of(rune)
 				pc_open += size_of(rune)
-				break add_global
+				break optimize_opening
 
 
 			case .Rune_Class:
 			case .Rune_Class:
 				inject_at(&code, pc_open, Opcode.Wait_For_Rune_Class)
 				inject_at(&code, pc_open, Opcode.Wait_For_Rune_Class)
 				pc_open += size_of(Opcode)
 				pc_open += size_of(Opcode)
 				inject_at(&code, pc_open, Opcode(code[pc + size_of(Opcode) + pc_open]))
 				inject_at(&code, pc_open, Opcode(code[pc + size_of(Opcode) + pc_open]))
 				pc_open += size_of(u8)
 				pc_open += size_of(u8)
-				break add_global
+				break optimize_opening
 
 
 			case .Rune_Class_Negated:
 			case .Rune_Class_Negated:
 				inject_at(&code, pc_open, Opcode.Wait_For_Rune_Class_Negated)
 				inject_at(&code, pc_open, Opcode.Wait_For_Rune_Class_Negated)
 				pc_open += size_of(Opcode)
 				pc_open += size_of(Opcode)
 				inject_at(&code, pc_open, Opcode(code[pc + size_of(Opcode) + pc_open]))
 				inject_at(&code, pc_open, Opcode(code[pc + size_of(Opcode) + pc_open]))
 				pc_open += size_of(u8)
 				pc_open += size_of(u8)
-				break add_global
+				break optimize_opening
 
 
 			case .Save:
 			case .Save:
 				continue
 				continue
+
+			case .Assert_Start:
+				break optimize_opening
+
 			case:
 			case:
 				break seek_loop
 				break seek_loop
 			}
 			}

+ 62 - 9
core/text/regex/regex.odin

@@ -77,6 +77,8 @@ Match_Iterator :: struct {
 	vm:       virtual_machine.Machine,
 	vm:       virtual_machine.Machine,
 	idx:      int,
 	idx:      int,
 	temp:     runtime.Allocator,
 	temp:     runtime.Allocator,
+	threads:  int,
+	done:     bool,
 }
 }
 
 
 /*
 /*
@@ -101,7 +103,6 @@ create :: proc(
 	permanent_allocator := context.allocator,
 	permanent_allocator := context.allocator,
 	temporary_allocator := context.temp_allocator,
 	temporary_allocator := context.temp_allocator,
 ) -> (result: Regular_Expression, err: Error) {
 ) -> (result: Regular_Expression, err: Error) {
-
 	// For the sake of speed and simplicity, we first run all the intermediate
 	// For the sake of speed and simplicity, we first run all the intermediate
 	// processes such as parsing and compilation through the temporary
 	// processes such as parsing and compilation through the temporary
 	// allocator.
 	// allocator.
@@ -166,7 +167,6 @@ to escape the delimiter if found in the middle of the string.
 
 
 All runes after the closing delimiter will be parsed as flags:
 All runes after the closing delimiter will be parsed as flags:
 
 
-- 'g': Global
 - 'm': Multiline
 - 'm': Multiline
 - 'i': Case_Insensitive
 - 'i': Case_Insensitive
 - 'x': Ignore_Whitespace
 - 'x': Ignore_Whitespace
@@ -243,7 +243,6 @@ create_by_user :: proc(
 	// to `end` here.
 	// to `end` here.
 	for r in pattern[start + end:] {
 	for r in pattern[start + end:] {
 		switch r {
 		switch r {
-		case 'g': flags += { .Global }
 		case 'm': flags += { .Multiline }
 		case 'm': flags += { .Multiline }
 		case 'i': flags += { .Case_Insensitive }
 		case 'i': flags += { .Case_Insensitive }
 		case 'x': flags += { .Ignore_Whitespace }
 		case 'x': flags += { .Ignore_Whitespace }
@@ -282,8 +281,6 @@ create_iterator :: proc(
 	permanent_allocator := context.allocator,
 	permanent_allocator := context.allocator,
 	temporary_allocator := context.temp_allocator,
 	temporary_allocator := context.temp_allocator,
 ) -> (result: Match_Iterator, err: Error) {
 ) -> (result: Match_Iterator, err: Error) {
-	flags := flags
-	flags += {.Global} // We're iterating over a string, so the next match could start anywhere
 
 
 	if .Multiline in flags {
 	if .Multiline in flags {
 		return {}, .Unsupported_Flag
 		return {}, .Unsupported_Flag
@@ -294,6 +291,7 @@ create_iterator :: proc(
 	result.temp          = temporary_allocator
 	result.temp          = temporary_allocator
 	result.vm            = virtual_machine.create(result.regex.program, str)
 	result.vm            = virtual_machine.create(result.regex.program, str)
 	result.vm.class_data = result.regex.class_data
 	result.vm.class_data = result.regex.class_data
+	result.threads       = max(1, virtual_machine.opcode_count(result.vm.code) - 1)
 
 
 	return
 	return
 }
 }
@@ -457,8 +455,27 @@ match_iterator :: proc(it: ^Match_Iterator) -> (result: Capture, index: int, ok:
 	assert(len(it.capture.pos) >= common.MAX_CAPTURE_GROUPS,
 	assert(len(it.capture.pos) >= common.MAX_CAPTURE_GROUPS,
 		"Pre-allocated RegEx capture `pos` must be at least 10 elements long.")
 		"Pre-allocated RegEx capture `pos` must be at least 10 elements long.")
 
 
+	// Guard against situations in which the iterator should finish.
+	if it.done {
+		return
+	}
+
 	runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD()
 	runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD()
 
 
+	if it.idx > 0 {
+		// Reset the state needed to `virtual_machine.run` again.
+		it.vm.top_thread        = 0
+		it.vm.current_rune      = rune(0)
+		it.vm.current_rune_size = 0
+		for i in 0..<it.threads {
+			it.vm.threads[i]      = {}
+			it.vm.next_threads[i] = {}
+		}
+	}
+
+	// Take note of where the string pointer is before we start.
+	sp_before := it.vm.string_pointer
+
 	saved: ^[2 * common.MAX_CAPTURE_GROUPS]int
 	saved: ^[2 * common.MAX_CAPTURE_GROUPS]int
 	{
 	{
 		context.allocator = it.temp
 		context.allocator = it.temp
@@ -469,6 +486,28 @@ match_iterator :: proc(it: ^Match_Iterator) -> (result: Capture, index: int, ok:
 		}
 		}
 	}
 	}
 
 
+	if !ok {
+		// Match failed, bail out.
+		return
+	}
+
+	if it.vm.string_pointer == sp_before {
+		// The string pointer did not move, but there was a match.
+		//
+		// At this point, the pattern supplied to the iterator will infinitely
+		// loop if we do not intervene.
+		it.done = true
+	}
+	if it.vm.string_pointer == len(it.vm.memory) {
+		// The VM hit the end of the string.
+		//
+		// We do not check at the start, because a match of pattern `$`
+		// against string "" is valid and must return a match.
+		//
+		// This check prevents a double-match of `$` against a non-empty string.
+		it.done = true
+	}
+
 	str := string(it.vm.memory)
 	str := string(it.vm.memory)
 	num_groups: int
 	num_groups: int
 
 
@@ -488,9 +527,7 @@ match_iterator :: proc(it: ^Match_Iterator) -> (result: Capture, index: int, ok:
 		num_groups = n
 		num_groups = n
 	}
 	}
 
 
-	defer if ok {
-		it.idx += 1
-	}
+	defer it.idx += 1
 
 
 	if num_groups > 0 {
 	if num_groups > 0 {
 		result = {it.capture.pos[:num_groups], it.capture.groups[:num_groups]}
 		result = {it.capture.pos[:num_groups], it.capture.groups[:num_groups]}
@@ -504,8 +541,24 @@ match :: proc {
 	match_iterator,
 	match_iterator,
 }
 }
 
 
+/*
+Reset an iterator, allowing it to be run again as if new.
+
+Inputs:
+- it: The iterator to reset.
+*/
 reset :: proc(it: ^Match_Iterator) {
 reset :: proc(it: ^Match_Iterator) {
-	it.idx    = 0
+	it.done                 = false
+	it.idx                  = 0
+	it.vm.string_pointer    = 0
+
+	it.vm.top_thread        = 0
+	it.vm.current_rune      = rune(0)
+	it.vm.current_rune_size = 0
+	for i in 0..<it.threads {
+		it.vm.threads[i]      = {}
+		it.vm.next_threads[i] = {}
+	}
 }
 }
 
 
 /*
 /*

+ 3 - 3
core/text/regex/virtual_machine/virtual_machine.odin

@@ -329,10 +329,10 @@ add_thread :: proc(vm: ^Machine, saved: ^[2 * common.MAX_CAPTURE_GROUPS]int, pc:
 
 
 run :: proc(vm: ^Machine, $UNICODE_MODE: bool) -> (saved: ^[2 * common.MAX_CAPTURE_GROUPS]int, ok: bool) #no_bounds_check {
 run :: proc(vm: ^Machine, $UNICODE_MODE: bool) -> (saved: ^[2 * common.MAX_CAPTURE_GROUPS]int, ok: bool) #no_bounds_check {
 	when UNICODE_MODE {
 	when UNICODE_MODE {
-		vm.next_rune, vm.next_rune_size = utf8.decode_rune_in_string(vm.memory)
+		vm.next_rune, vm.next_rune_size = utf8.decode_rune_in_string(vm.memory[vm.string_pointer:])
 	} else {
 	} else {
 		if len(vm.memory) > 0 {
 		if len(vm.memory) > 0 {
-			vm.next_rune = cast(rune)vm.memory[0]
+			vm.next_rune = cast(rune)vm.memory[vm.string_pointer]
 			vm.next_rune_size = 1
 			vm.next_rune_size = 1
 		}
 		}
 	}
 	}
@@ -652,4 +652,4 @@ destroy :: proc(vm: Machine, allocator := context.allocator) {
 	delete(vm.busy_map)
 	delete(vm.busy_map)
 	free(vm.threads)
 	free(vm.threads)
 	free(vm.next_threads)
 	free(vm.next_threads)
-}
+}

+ 4 - 2
core/text/scanner/scanner.odin

@@ -285,6 +285,7 @@ scan_number :: proc(s: ^Scanner, ch: rune, seen_dot: bool) -> (rune, rune) {
 		case 'o': return "octal literal"
 		case 'o': return "octal literal"
 		case 'z': return "dozenal literal"
 		case 'z': return "dozenal literal"
 		case 'x': return "hexadecimal literal"
 		case 'x': return "hexadecimal literal"
+		case 'h': return "hexadecimal literal"
 		}
 		}
 		return "decimal literal"
 		return "decimal literal"
 	}
 	}
@@ -360,7 +361,8 @@ scan_number :: proc(s: ^Scanner, ch: rune, seen_dot: bool) -> (rune, rune) {
 					base, prefix = 12, 'z'
 					base, prefix = 12, 'z'
 				case 'h':
 				case 'h':
 					tok = Float
 					tok = Float
-					fallthrough
+					ch = advance(s)
+					base, prefix = 16, 'h'
 				case 'x':
 				case 'x':
 					ch = advance(s)
 					ch = advance(s)
 					base, prefix = 16, 'x'
 					base, prefix = 16, 'x'
@@ -447,7 +449,7 @@ scan_string :: proc(s: ^Scanner, quote: rune) -> (n: int) {
 	ch := advance(s)
 	ch := advance(s)
 	for ch != quote {
 	for ch != quote {
 		if ch == '\n' || ch < 0 {
 		if ch == '\n' || ch < 0 {
-			error(s, "literal no terminated")
+			error(s, "literal not terminated")
 			return
 			return
 		}
 		}
 		if ch == '\\' {
 		if ch == '\\' {

+ 5 - 2
examples/all/all_main.odin

@@ -58,7 +58,6 @@ import trace            "core:debug/trace"
 import dynlib           "core:dynlib"
 import dynlib           "core:dynlib"
 import net              "core:net"
 import net              "core:net"
 
 
-import ansi             "core:encoding/ansi"
 import base32           "core:encoding/base32"
 import base32           "core:encoding/base32"
 import base64           "core:encoding/base64"
 import base64           "core:encoding/base64"
 import cbor             "core:encoding/cbor"
 import cbor             "core:encoding/cbor"
@@ -129,6 +128,9 @@ import strings          "core:strings"
 import sync             "core:sync"
 import sync             "core:sync"
 import testing          "core:testing"
 import testing          "core:testing"
 
 
+import terminal         "core:terminal"
+import ansi             "core:terminal/ansi"
+
 import edit             "core:text/edit"
 import edit             "core:text/edit"
 import i18n             "core:text/i18n"
 import i18n             "core:text/i18n"
 import match            "core:text/match"
 import match            "core:text/match"
@@ -201,7 +203,6 @@ _ :: pe
 _ :: trace
 _ :: trace
 _ :: dynlib
 _ :: dynlib
 _ :: net
 _ :: net
-_ :: ansi
 _ :: base32
 _ :: base32
 _ :: base64
 _ :: base64
 _ :: csv
 _ :: csv
@@ -257,6 +258,8 @@ _ :: strconv
 _ :: strings
 _ :: strings
 _ :: sync
 _ :: sync
 _ :: testing
 _ :: testing
+_ :: terminal
+_ :: ansi
 _ :: scanner
 _ :: scanner
 _ :: i18n
 _ :: i18n
 _ :: match
 _ :: match

+ 33 - 9
src/build_settings.cpp

@@ -441,6 +441,7 @@ struct BuildContext {
 	String extra_assembler_flags;
 	String extra_assembler_flags;
 	String microarch;
 	String microarch;
 	BuildModeKind build_mode;
 	BuildModeKind build_mode;
+	bool   keep_executable;
 	bool   generate_docs;
 	bool   generate_docs;
 	bool   custom_optimization_level;
 	bool   custom_optimization_level;
 	i32    optimization_level;
 	i32    optimization_level;
@@ -2209,11 +2210,34 @@ gb_internal bool init_build_paths(String init_filename) {
 			while (output_name.len > 0 && (output_name[output_name.len-1] == '/' || output_name[output_name.len-1] == '\\')) {
 			while (output_name.len > 0 && (output_name[output_name.len-1] == '/' || output_name[output_name.len-1] == '\\')) {
 				output_name.len -= 1;
 				output_name.len -= 1;
 			}
 			}
+			// Only trim the extension if it's an Odin source file.
+			// This lets people build folders with extensions or files beginning with dots.
+			if (path_extension(output_name) == ".odin" && !path_is_directory(output_name)) {
+				output_name = remove_extension_from_path(output_name);
+			}
 			output_name = remove_directory_from_path(output_name);
 			output_name = remove_directory_from_path(output_name);
-			output_name = remove_extension_from_path(output_name);
 			output_name = copy_string(ha, string_trim_whitespace(output_name));
 			output_name = copy_string(ha, string_trim_whitespace(output_name));
-			output_path = path_from_string(ha, output_name);
-			
+			// This is `path_from_string` without the extension trimming.
+			Path res = {};
+			if (output_name.len > 0) {
+				String fullpath = path_to_full_path(ha, output_name);
+				defer (gb_free(ha, fullpath.text));
+
+				res.basename = directory_from_path(fullpath);
+				res.basename = copy_string(ha, res.basename);
+
+				if (path_is_directory(fullpath)) {
+					if (res.basename.len > 0 && res.basename.text[res.basename.len - 1] == '/') {
+						res.basename.len--;
+					}
+				} else {
+					isize name_start = (res.basename.len > 0) ? res.basename.len + 1 : res.basename.len;
+					res.name         = substring(fullpath, name_start, fullpath.len);
+					res.name         = copy_string(ha, res.name);
+				}
+			}
+			output_path = res;
+
 			// Note(Dragos): This is a fix for empty filenames
 			// Note(Dragos): This is a fix for empty filenames
 			// Turn the trailing folder into the file name
 			// Turn the trailing folder into the file name
 			if (output_path.name.len == 0) {
 			if (output_path.name.len == 0) {
@@ -2278,9 +2302,10 @@ gb_internal bool init_build_paths(String init_filename) {
 		case TargetOs_windows:
 		case TargetOs_windows:
 		case TargetOs_linux:
 		case TargetOs_linux:
 		case TargetOs_darwin:
 		case TargetOs_darwin:
+		case TargetOs_freebsd:
 			break;
 			break;
 		default:
 		default:
-			gb_printf_err("-sanitize:address is only supported on windows, linux, and darwin\n");
+			gb_printf_err("-sanitize:address is only supported on Windows, Linux, Darwin, and FreeBSD\n");
 			return false;
 			return false;
 		}
 		}
 	}
 	}
@@ -2288,12 +2313,10 @@ gb_internal bool init_build_paths(String init_filename) {
 	if (build_context.sanitizer_flags & SanitizerFlag_Memory) {
 	if (build_context.sanitizer_flags & SanitizerFlag_Memory) {
 		switch (build_context.metrics.os) {
 		switch (build_context.metrics.os) {
 		case TargetOs_linux:
 		case TargetOs_linux:
+		case TargetOs_freebsd:
 			break;
 			break;
 		default:
 		default:
-			gb_printf_err("-sanitize:memory is only supported on linux\n");
-			return false;
-		}
-		if (build_context.metrics.os != TargetOs_linux) {
+			gb_printf_err("-sanitize:memory is only supported on Linux and FreeBSD\n");
 			return false;
 			return false;
 		}
 		}
 	}
 	}
@@ -2302,9 +2325,10 @@ gb_internal bool init_build_paths(String init_filename) {
 		switch (build_context.metrics.os) {
 		switch (build_context.metrics.os) {
 		case TargetOs_linux:
 		case TargetOs_linux:
 		case TargetOs_darwin:
 		case TargetOs_darwin:
+		case TargetOs_freebsd:
 			break;
 			break;
 		default:
 		default:
-			gb_printf_err("-sanitize:thread is only supported on linux and darwin\n");
+			gb_printf_err("-sanitize:thread is only supported on Linux, Darwin, and FreeBSD\n");
 			return false;
 			return false;
 		}
 		}
 	}
 	}

+ 14 - 6
src/check_expr.cpp

@@ -5461,8 +5461,18 @@ gb_internal Entity *check_selector(CheckerContext *c, Operand *operand, Ast *nod
 		}
 		}
 	}
 	}
 
 
+	if (operand->type && is_type_simd_vector(type_deref(operand->type))) {
+		String field_name = selector->Ident.token.string;
+		if (field_name.len == 1) {
+			error(op_expr, "Extracting an element from a #simd array using .%.*s syntax is disallowed, prefer `simd.extract`", LIT(field_name));
+		} else {
+			error(op_expr, "Extracting elements from a #simd array using .%.*s syntax is disallowed, prefer `swizzle`", LIT(field_name));
+		}
+		return nullptr;
+	}
+
 	if (entity == nullptr && selector->kind == Ast_Ident && operand->type != nullptr &&
 	if (entity == nullptr && selector->kind == Ast_Ident && operand->type != nullptr &&
-	    (is_type_array(type_deref(operand->type)) || is_type_simd_vector(type_deref(operand->type)))) {
+	    (is_type_array(type_deref(operand->type)))) {
 		String field_name = selector->Ident.token.string;
 		String field_name = selector->Ident.token.string;
 		if (1 < field_name.len && field_name.len <= 4) {
 		if (1 < field_name.len && field_name.len <= 4) {
 			u8 swizzles_xyzw[4] = {'x', 'y', 'z', 'w'};
 			u8 swizzles_xyzw[4] = {'x', 'y', 'z', 'w'};
@@ -5517,7 +5527,7 @@ gb_internal Entity *check_selector(CheckerContext *c, Operand *operand, Ast *nod
 
 
 			Type *original_type = operand->type;
 			Type *original_type = operand->type;
 			Type *array_type = base_type(type_deref(original_type));
 			Type *array_type = base_type(type_deref(original_type));
-			GB_ASSERT(array_type->kind == Type_Array || array_type->kind == Type_SimdVector);
+			GB_ASSERT(array_type->kind == Type_Array);
 
 
 			i64 array_count = get_array_type_count(array_type);
 			i64 array_count = get_array_type_count(array_type);
 
 
@@ -5558,10 +5568,6 @@ gb_internal Entity *check_selector(CheckerContext *c, Operand *operand, Ast *nod
 				break;
 				break;
 			}
 			}
 
 
-			if (array_type->kind == Type_SimdVector) {
-				operand->mode = Addressing_Value;
-			}
-
 			Entity *swizzle_entity = alloc_entity_variable(nullptr, make_token_ident(field_name), operand->type, EntityState_Resolved);
 			Entity *swizzle_entity = alloc_entity_variable(nullptr, make_token_ident(field_name), operand->type, EntityState_Resolved);
 			add_type_and_value(c, operand->expr, operand->mode, operand->type, operand->value);
 			add_type_and_value(c, operand->expr, operand->mode, operand->type, operand->value);
 			return swizzle_entity;
 			return swizzle_entity;
@@ -8070,7 +8076,9 @@ gb_internal ExprKind check_call_expr(CheckerContext *c, Operand *operand, Ast *c
 
 
 	if (pt->kind == Type_Proc && pt->Proc.calling_convention == ProcCC_Odin) {
 	if (pt->kind == Type_Proc && pt->Proc.calling_convention == ProcCC_Odin) {
 		if ((c->scope->flags & ScopeFlag_ContextDefined) == 0) {
 		if ((c->scope->flags & ScopeFlag_ContextDefined) == 0) {
+			ERROR_BLOCK();
 			error(call, "'context' has not been defined within this scope, but is required for this procedure call");
 			error(call, "'context' has not been defined within this scope, but is required for this procedure call");
+			error_line("\tSuggestion: 'context = runtime.default_context()'");
 		}
 		}
 	}
 	}
 
 

+ 15 - 0
src/linker.cpp

@@ -801,6 +801,21 @@ try_cross_linking:;
 					// This points the linker to where the entry point is
 					// This points the linker to where the entry point is
 					link_settings = gb_string_appendc(link_settings, "-e _main ");
 					link_settings = gb_string_appendc(link_settings, "-e _main ");
 				}
 				}
+			} else if (build_context.metrics.os == TargetOs_freebsd) {
+				if (build_context.sanitizer_flags & (SanitizerFlag_Address | SanitizerFlag_Memory)) {
+					// It's imperative that `pthread` is linked before `libc`,
+					// otherwise ASan/MSan will be unable to call `pthread_key_create`
+					// because FreeBSD's `libthr` implementation of `pthread`
+					// needs to replace the relevant stubs first.
+					//
+					// (Presumably TSan implements its own `pthread` interface,
+					//  which is why it isn't required.)
+					//
+					// See: https://reviews.llvm.org/D39254
+					platform_lib_str = gb_string_appendc(platform_lib_str, "-lpthread ");
+				}
+				// FreeBSD pkg installs third-party shared libraries in /usr/local/lib.
+				platform_lib_str = gb_string_appendc(platform_lib_str, "-Wl,-L/usr/local/lib ");
 			} else if (build_context.metrics.os == TargetOs_openbsd) {
 			} else if (build_context.metrics.os == TargetOs_openbsd) {
 				// OpenBSD ports install shared libraries in /usr/local/lib. Also, we must explicitly link libpthread.
 				// OpenBSD ports install shared libraries in /usr/local/lib. Also, we must explicitly link libpthread.
 				platform_lib_str = gb_string_appendc(platform_lib_str, "-lpthread -Wl,-L/usr/local/lib ");
 				platform_lib_str = gb_string_appendc(platform_lib_str, "-lpthread -Wl,-L/usr/local/lib ");

+ 24 - 4
src/llvm_backend.cpp

@@ -1907,6 +1907,10 @@ gb_internal lbProcedure *lb_create_startup_runtime(lbModule *main_module, lbProc
 	lb_add_attribute_to_proc(p->module, p->value, "optnone");
 	lb_add_attribute_to_proc(p->module, p->value, "optnone");
 	lb_add_attribute_to_proc(p->module, p->value, "noinline");
 	lb_add_attribute_to_proc(p->module, p->value, "noinline");
 
 
+	// Make sure shared libraries call their own runtime startup on Linux.
+	LLVMSetVisibility(p->value, LLVMHiddenVisibility);
+	LLVMSetLinkage(p->value, LLVMWeakAnyLinkage);
+
 	lb_begin_procedure_body(p);
 	lb_begin_procedure_body(p);
 
 
 	lb_setup_type_info_data(main_module);
 	lb_setup_type_info_data(main_module);
@@ -2016,6 +2020,10 @@ gb_internal lbProcedure *lb_create_cleanup_runtime(lbModule *main_module) { // C
 	lb_add_attribute_to_proc(p->module, p->value, "optnone");
 	lb_add_attribute_to_proc(p->module, p->value, "optnone");
 	lb_add_attribute_to_proc(p->module, p->value, "noinline");
 	lb_add_attribute_to_proc(p->module, p->value, "noinline");
 
 
+	// Make sure shared libraries call their own runtime cleanup on Linux.
+	LLVMSetVisibility(p->value, LLVMHiddenVisibility);
+	LLVMSetLinkage(p->value, LLVMWeakAnyLinkage);
+
 	lb_begin_procedure_body(p);
 	lb_begin_procedure_body(p);
 
 
 	CheckerInfo *info = main_module->gen->info;
 	CheckerInfo *info = main_module->gen->info;
@@ -3417,36 +3425,48 @@ gb_internal bool lb_generate_code(lbGenerator *gen) {
 
 
 
 
 	if (build_context.sanitizer_flags & SanitizerFlag_Address) {
 	if (build_context.sanitizer_flags & SanitizerFlag_Address) {
-		if (build_context.metrics.os == TargetOs_windows) {
+		switch (build_context.metrics.os) {
+		case TargetOs_windows: {
 			auto paths = array_make<String>(heap_allocator(), 0, 1);
 			auto paths = array_make<String>(heap_allocator(), 0, 1);
 			String path = concatenate_strings(permanent_allocator(), build_context.ODIN_ROOT, str_lit("\\bin\\llvm\\windows\\clang_rt.asan-x86_64.lib"));
 			String path = concatenate_strings(permanent_allocator(), build_context.ODIN_ROOT, str_lit("\\bin\\llvm\\windows\\clang_rt.asan-x86_64.lib"));
 			array_add(&paths, path);
 			array_add(&paths, path);
 			Entity *lib = alloc_entity_library_name(nullptr, make_token_ident("asan_lib"), nullptr, slice_from_array(paths), str_lit("asan_lib"));
 			Entity *lib = alloc_entity_library_name(nullptr, make_token_ident("asan_lib"), nullptr, slice_from_array(paths), str_lit("asan_lib"));
 			array_add(&gen->foreign_libraries, lib);
 			array_add(&gen->foreign_libraries, lib);
-		} else if (build_context.metrics.os == TargetOs_darwin || build_context.metrics.os == TargetOs_linux) {
+		} break;
+		case TargetOs_darwin:
+		case TargetOs_linux:
+		case TargetOs_freebsd:
 			if (!build_context.extra_linker_flags.text) {
 			if (!build_context.extra_linker_flags.text) {
 				build_context.extra_linker_flags = str_lit("-fsanitize=address");
 				build_context.extra_linker_flags = str_lit("-fsanitize=address");
 			} else {
 			} else {
 				build_context.extra_linker_flags = concatenate_strings(permanent_allocator(), build_context.extra_linker_flags, str_lit(" -fsanitize=address"));
 				build_context.extra_linker_flags = concatenate_strings(permanent_allocator(), build_context.extra_linker_flags, str_lit(" -fsanitize=address"));
 			}
 			}
+			break;
 		}
 		}
 	}
 	}
 	if (build_context.sanitizer_flags & SanitizerFlag_Memory) {
 	if (build_context.sanitizer_flags & SanitizerFlag_Memory) {
-		if (build_context.metrics.os == TargetOs_darwin || build_context.metrics.os == TargetOs_linux) {
+		switch (build_context.metrics.os) {
+		case TargetOs_linux:
+		case TargetOs_freebsd:
 			if (!build_context.extra_linker_flags.text) {
 			if (!build_context.extra_linker_flags.text) {
 				build_context.extra_linker_flags = str_lit("-fsanitize=memory");
 				build_context.extra_linker_flags = str_lit("-fsanitize=memory");
 			} else {
 			} else {
 				build_context.extra_linker_flags = concatenate_strings(permanent_allocator(), build_context.extra_linker_flags, str_lit(" -fsanitize=memory"));
 				build_context.extra_linker_flags = concatenate_strings(permanent_allocator(), build_context.extra_linker_flags, str_lit(" -fsanitize=memory"));
 			}
 			}
+			break;
 		}
 		}
 	}
 	}
 	if (build_context.sanitizer_flags & SanitizerFlag_Thread) {
 	if (build_context.sanitizer_flags & SanitizerFlag_Thread) {
-		if (build_context.metrics.os == TargetOs_darwin || build_context.metrics.os == TargetOs_linux) {
+		switch (build_context.metrics.os) {
+		case TargetOs_darwin:
+		case TargetOs_linux:
+		case TargetOs_freebsd:
 			if (!build_context.extra_linker_flags.text) {
 			if (!build_context.extra_linker_flags.text) {
 				build_context.extra_linker_flags = str_lit("-fsanitize=thread");
 				build_context.extra_linker_flags = str_lit("-fsanitize=thread");
 			} else {
 			} else {
 				build_context.extra_linker_flags = concatenate_strings(permanent_allocator(), build_context.extra_linker_flags, str_lit(" -fsanitize=thread"));
 				build_context.extra_linker_flags = concatenate_strings(permanent_allocator(), build_context.extra_linker_flags, str_lit(" -fsanitize=thread"));
 			}
 			}
+			break;
 		}
 		}
 	}
 	}
 
 

+ 89 - 50
src/main.cpp

@@ -312,6 +312,7 @@ enum BuildFlagKind {
 	BuildFlag_Collection,
 	BuildFlag_Collection,
 	BuildFlag_Define,
 	BuildFlag_Define,
 	BuildFlag_BuildMode,
 	BuildFlag_BuildMode,
+	BuildFlag_KeepExecutable,
 	BuildFlag_Target,
 	BuildFlag_Target,
 	BuildFlag_Subtarget,
 	BuildFlag_Subtarget,
 	BuildFlag_Debug,
 	BuildFlag_Debug,
@@ -531,6 +532,7 @@ gb_internal bool parse_build_flags(Array<String> args) {
 	add_flag(&build_flags, BuildFlag_Collection,              str_lit("collection"),                BuildFlagParam_String,  Command__does_check);
 	add_flag(&build_flags, BuildFlag_Collection,              str_lit("collection"),                BuildFlagParam_String,  Command__does_check);
 	add_flag(&build_flags, BuildFlag_Define,                  str_lit("define"),                    BuildFlagParam_String,  Command__does_check, true);
 	add_flag(&build_flags, BuildFlag_Define,                  str_lit("define"),                    BuildFlagParam_String,  Command__does_check, true);
 	add_flag(&build_flags, BuildFlag_BuildMode,               str_lit("build-mode"),                BuildFlagParam_String,  Command__does_build); // Commands_build is not used to allow for a better error message
 	add_flag(&build_flags, BuildFlag_BuildMode,               str_lit("build-mode"),                BuildFlagParam_String,  Command__does_build); // Commands_build is not used to allow for a better error message
+	add_flag(&build_flags, BuildFlag_KeepExecutable,          str_lit("keep-executable"),           BuildFlagParam_None,    Command__does_build | Command_test);
 	add_flag(&build_flags, BuildFlag_Target,                  str_lit("target"),                    BuildFlagParam_String,  Command__does_check);
 	add_flag(&build_flags, BuildFlag_Target,                  str_lit("target"),                    BuildFlagParam_String,  Command__does_check);
 	add_flag(&build_flags, BuildFlag_Subtarget,               str_lit("subtarget"),                 BuildFlagParam_String,  Command__does_check);
 	add_flag(&build_flags, BuildFlag_Subtarget,               str_lit("subtarget"),                 BuildFlagParam_String,  Command__does_check);
 	add_flag(&build_flags, BuildFlag_Debug,                   str_lit("debug"),                     BuildFlagParam_None,    Command__does_check);
 	add_flag(&build_flags, BuildFlag_Debug,                   str_lit("debug"),                     BuildFlagParam_None,    Command__does_check);
@@ -1193,6 +1195,9 @@ gb_internal bool parse_build_flags(Array<String> args) {
 
 
 							break;
 							break;
 						}
 						}
+						case BuildFlag_KeepExecutable:
+							build_context.keep_executable = true;
+							break;
 
 
 						case BuildFlag_Debug:
 						case BuildFlag_Debug:
 							build_context.ODIN_DEBUG = true;
 							build_context.ODIN_DEBUG = true;
@@ -2364,20 +2369,26 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		if (print_flag("-build-mode:<mode>")) {
 		if (print_flag("-build-mode:<mode>")) {
 			print_usage_line(2, "Sets the build mode.");
 			print_usage_line(2, "Sets the build mode.");
 			print_usage_line(2, "Available options:");
 			print_usage_line(2, "Available options:");
-			print_usage_line(3, "-build-mode:exe         Builds as an executable.");
-			print_usage_line(3, "-build-mode:test        Builds as an executable that executes tests.");
-			print_usage_line(3, "-build-mode:dll         Builds as a dynamically linked library.");
-			print_usage_line(3, "-build-mode:shared      Builds as a dynamically linked library.");
-			print_usage_line(3, "-build-mode:dynamic     Builds as a dynamically linked library.");
-			print_usage_line(3, "-build-mode:lib         Builds as a statically linked library.");
-			print_usage_line(3, "-build-mode:static      Builds as a statically linked library.");
-			print_usage_line(3, "-build-mode:obj         Builds as an object file.");
-			print_usage_line(3, "-build-mode:object      Builds as an object file.");
-			print_usage_line(3, "-build-mode:assembly    Builds as an assembly file.");
-			print_usage_line(3, "-build-mode:assembler   Builds as an assembly file.");
-			print_usage_line(3, "-build-mode:asm         Builds as an assembly file.");
-			print_usage_line(3, "-build-mode:llvm-ir     Builds as an LLVM IR file.");
-			print_usage_line(3, "-build-mode:llvm        Builds as an LLVM IR file.");
+				print_usage_line(3, "-build-mode:exe         Builds as an executable.");
+				print_usage_line(3, "-build-mode:test        Builds as an executable that executes tests.");
+				print_usage_line(3, "-build-mode:dll         Builds as a dynamically linked library.");
+				print_usage_line(3, "-build-mode:shared      Builds as a dynamically linked library.");
+				print_usage_line(3, "-build-mode:dynamic     Builds as a dynamically linked library.");
+				print_usage_line(3, "-build-mode:lib         Builds as a statically linked library.");
+				print_usage_line(3, "-build-mode:static      Builds as a statically linked library.");
+				print_usage_line(3, "-build-mode:obj         Builds as an object file.");
+				print_usage_line(3, "-build-mode:object      Builds as an object file.");
+				print_usage_line(3, "-build-mode:assembly    Builds as an assembly file.");
+				print_usage_line(3, "-build-mode:assembler   Builds as an assembly file.");
+				print_usage_line(3, "-build-mode:asm         Builds as an assembly file.");
+				print_usage_line(3, "-build-mode:llvm-ir     Builds as an LLVM IR file.");
+				print_usage_line(3, "-build-mode:llvm        Builds as an LLVM IR file.");
+		}
+	}
+
+	if (test_only) {
+		if (print_flag("-build-only")) {
+			print_usage_line(2, "Only builds the test executable; does not automatically run it afterwards.");
 		}
 		}
 	}
 	}
 
 
@@ -2386,16 +2397,16 @@ gb_internal int print_show_help(String const arg0, String command, String option
 			print_usage_line(2, "Defines a library collection used for imports.");
 			print_usage_line(2, "Defines a library collection used for imports.");
 			print_usage_line(2, "Example: -collection:shared=dir/to/shared");
 			print_usage_line(2, "Example: -collection:shared=dir/to/shared");
 			print_usage_line(2, "Usage in Code:");
 			print_usage_line(2, "Usage in Code:");
-			print_usage_line(3, "import \"shared:foo\"");
+				print_usage_line(3, "import \"shared:foo\"");
 		}
 		}
 
 
 		if (print_flag("-custom-attribute:<string>")) {
 		if (print_flag("-custom-attribute:<string>")) {
 			print_usage_line(2, "Add a custom attribute which will be ignored if it is unknown.");
 			print_usage_line(2, "Add a custom attribute which will be ignored if it is unknown.");
 			print_usage_line(2, "This can be used with metaprogramming tools.");
 			print_usage_line(2, "This can be used with metaprogramming tools.");
 			print_usage_line(2, "Examples:");
 			print_usage_line(2, "Examples:");
-			print_usage_line(3, "-custom-attribute:my_tag");
-			print_usage_line(3, "-custom-attribute:my_tag,the_other_thing");
-			print_usage_line(3, "-custom-attribute:my_tag -custom-attribute:the_other_thing");
+				print_usage_line(3, "-custom-attribute:my_tag");
+				print_usage_line(3, "-custom-attribute:my_tag,the_other_thing");
+				print_usage_line(3, "-custom-attribute:my_tag -custom-attribute:the_other_thing");
 		}
 		}
 	}
 	}
 
 
@@ -2418,7 +2429,7 @@ gb_internal int print_show_help(String const arg0, String command, String option
 			print_usage_line(2, "Defines a scalar boolean, integer or string as global constant.");
 			print_usage_line(2, "Defines a scalar boolean, integer or string as global constant.");
 			print_usage_line(2, "Example: -define:SPAM=123");
 			print_usage_line(2, "Example: -define:SPAM=123");
 			print_usage_line(2, "Usage in code:");
 			print_usage_line(2, "Usage in code:");
-			print_usage_line(3, "#config(SPAM, default_value)");
+				print_usage_line(3, "#config(SPAM, default_value)");
 		}
 		}
 	}
 	}
 
 
@@ -2453,9 +2464,9 @@ gb_internal int print_show_help(String const arg0, String command, String option
 	if (check) {
 	if (check) {
 		if (print_flag("-error-pos-style:<string>")) {
 		if (print_flag("-error-pos-style:<string>")) {
 			print_usage_line(2, "Available options:");
 			print_usage_line(2, "Available options:");
-			print_usage_line(3, "-error-pos-style:unix      file/path:45:3:");
-			print_usage_line(3, "-error-pos-style:odin      file/path(45:3)");
-			print_usage_line(3, "-error-pos-style:default   (Defaults to 'odin'.)");
+				print_usage_line(3, "-error-pos-style:unix      file/path:45:3:");
+				print_usage_line(3, "-error-pos-style:odin      file/path(45:3)");
+				print_usage_line(3, "-error-pos-style:default   (Defaults to 'odin'.)");
 		}
 		}
 
 
 		if (print_flag("-export-defineables:<filename>")) {
 		if (print_flag("-export-defineables:<filename>")) {
@@ -2466,8 +2477,8 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		if (print_flag("-export-dependencies:<format>")) {
 		if (print_flag("-export-dependencies:<format>")) {
 			print_usage_line(2, "Exports dependencies to one of a few formats. Requires `-export-dependencies-file`.");
 			print_usage_line(2, "Exports dependencies to one of a few formats. Requires `-export-dependencies-file`.");
 			print_usage_line(2, "Available options:");
 			print_usage_line(2, "Available options:");
-			print_usage_line(3, "-export-dependencies:make   Exports in Makefile format");
-			print_usage_line(3, "-export-dependencies:json   Exports in JSON format");
+				print_usage_line(3, "-export-dependencies:make   Exports in Makefile format");
+				print_usage_line(3, "-export-dependencies:json   Exports in JSON format");
 		}
 		}
 
 
 		if (print_flag("-export-dependencies-file:<filename>")) {
 		if (print_flag("-export-dependencies-file:<filename>")) {
@@ -2478,8 +2489,8 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		if (print_flag("-export-timings:<format>")) {
 		if (print_flag("-export-timings:<format>")) {
 			print_usage_line(2, "Exports timings to one of a few formats. Requires `-show-timings` or `-show-more-timings`.");
 			print_usage_line(2, "Exports timings to one of a few formats. Requires `-show-timings` or `-show-more-timings`.");
 			print_usage_line(2, "Available options:");
 			print_usage_line(2, "Available options:");
-			print_usage_line(3, "-export-timings:json   Exports compile time stats to JSON.");
-			print_usage_line(3, "-export-timings:csv    Exports compile time stats to CSV.");
+				print_usage_line(3, "-export-timings:json   Exports compile time stats to JSON.");
+				print_usage_line(3, "-export-timings:csv    Exports compile time stats to CSV.");
 		}
 		}
 
 
 		if (print_flag("-export-timings-file:<filename>")) {
 		if (print_flag("-export-timings-file:<filename>")) {
@@ -2543,6 +2554,14 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		}
 		}
 	}
 	}
 
 
+	if (test_only || run_or_build) {
+		if (print_flag("-keep-executable")) {
+			print_usage_line(2, "Keep the executable generated by `odin test` or `odin run` after running it. We clean it up by default.");
+			print_usage_line(2, "If you build your program or test using `odin build`, the compiler does not automatically execute");
+			print_usage_line(2, "the resulting program, and this option is not applicable.");
+		}
+	}
+
 	if (run_or_build) {
 	if (run_or_build) {
 		if (print_flag("-linker:<string>")) {
 		if (print_flag("-linker:<string>")) {
 			print_usage_line(2, "Specify the linker to use.");
 			print_usage_line(2, "Specify the linker to use.");
@@ -2569,9 +2588,9 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		if (print_flag("-microarch:<string>")) {
 		if (print_flag("-microarch:<string>")) {
 			print_usage_line(2, "Specifies the specific micro-architecture for the build in a string.");
 			print_usage_line(2, "Specifies the specific micro-architecture for the build in a string.");
 			print_usage_line(2, "Examples:");
 			print_usage_line(2, "Examples:");
-			print_usage_line(3, "-microarch:sandybridge");
-			print_usage_line(3, "-microarch:native");
-			print_usage_line(3, "-microarch:\"?\" for a list");
+				print_usage_line(3, "-microarch:sandybridge");
+				print_usage_line(3, "-microarch:native");
+				print_usage_line(3, "-microarch:\"?\" for a list");
 		}
 		}
 	}
 	}
 
 
@@ -2628,10 +2647,10 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		if (print_flag("-o:<string>")) {
 		if (print_flag("-o:<string>")) {
 			print_usage_line(2, "Sets the optimization mode for compilation.");
 			print_usage_line(2, "Sets the optimization mode for compilation.");
 			print_usage_line(2, "Available options:");
 			print_usage_line(2, "Available options:");
-			print_usage_line(3, "-o:none");
-			print_usage_line(3, "-o:minimal");
-			print_usage_line(3, "-o:size");
-			print_usage_line(3, "-o:speed");
+				print_usage_line(3, "-o:none");
+				print_usage_line(3, "-o:minimal");
+				print_usage_line(3, "-o:size");
+				print_usage_line(3, "-o:speed");
 			if (LB_USE_NEW_PASS_SYSTEM) {
 			if (LB_USE_NEW_PASS_SYSTEM) {
 				print_usage_line(3, "-o:aggressive (use this with caution)");
 				print_usage_line(3, "-o:aggressive (use this with caution)");
 			}
 			}
@@ -2682,10 +2701,10 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		if (print_flag("-reloc-mode:<string>")) {
 		if (print_flag("-reloc-mode:<string>")) {
 			print_usage_line(2, "Specifies the reloc mode.");
 			print_usage_line(2, "Specifies the reloc mode.");
 			print_usage_line(2, "Available options:");
 			print_usage_line(2, "Available options:");
-			print_usage_line(3, "-reloc-mode:default");
-			print_usage_line(3, "-reloc-mode:static");
-			print_usage_line(3, "-reloc-mode:pic");
-			print_usage_line(3, "-reloc-mode:dynamic-no-pic");
+				print_usage_line(3, "-reloc-mode:default");
+				print_usage_line(3, "-reloc-mode:static");
+				print_usage_line(3, "-reloc-mode:pic");
+				print_usage_line(3, "-reloc-mode:dynamic-no-pic");
 		}
 		}
 
 
 	#if defined(GB_SYSTEM_WINDOWS)
 	#if defined(GB_SYSTEM_WINDOWS)
@@ -2700,9 +2719,9 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		if (print_flag("-sanitize:<string>")) {
 		if (print_flag("-sanitize:<string>")) {
 			print_usage_line(2, "Enables sanitization analysis.");
 			print_usage_line(2, "Enables sanitization analysis.");
 			print_usage_line(2, "Available options:");
 			print_usage_line(2, "Available options:");
-			print_usage_line(3, "-sanitize:address");
-			print_usage_line(3, "-sanitize:memory");
-			print_usage_line(3, "-sanitize:thread");
+				print_usage_line(3, "-sanitize:address");
+				print_usage_line(3, "-sanitize:memory");
+				print_usage_line(3, "-sanitize:thread");
 			print_usage_line(2, "NOTE: This flag can be used multiple times.");
 			print_usage_line(2, "NOTE: This flag can be used multiple times.");
 		}
 		}
 	}
 	}
@@ -2763,17 +2782,32 @@ gb_internal int print_show_help(String const arg0, String command, String option
 			print_usage_line(2, "[Windows only]");
 			print_usage_line(2, "[Windows only]");
 			print_usage_line(2, "Defines the subsystem for the application.");
 			print_usage_line(2, "Defines the subsystem for the application.");
 			print_usage_line(2, "Available options:");
 			print_usage_line(2, "Available options:");
-		print_usage_line(3, "-subsystem:console");
-		print_usage_line(3, "-subsystem:windows");
+				print_usage_line(3, "-subsystem:console");
+				print_usage_line(3, "-subsystem:windows");
 		}
 		}
 	#endif
 	#endif
+	}
 
 
+	if (build) {
+		if (print_flag("-subtarget:<subtarget>")) {
+			print_usage_line(2, "[Darwin and Linux only]");
+			print_usage_line(2, "Available subtargets:");
+			String prefix = str_lit("-subtarget:");
+			for (u32 i = 1; i < Subtarget_COUNT; i++) {
+				String name   = subtarget_strings[i];
+				String help_string = concatenate_strings(temporary_allocator(), prefix, name);
+				print_usage_line(3, (const char *)help_string.text);
+			}
+		}
+	}
+
+	if (run_or_build) {
 		if (print_flag("-target-features:<string>")) {
 		if (print_flag("-target-features:<string>")) {
 			print_usage_line(2, "Specifies CPU features to enable on top of the enabled features implied by -microarch.");
 			print_usage_line(2, "Specifies CPU features to enable on top of the enabled features implied by -microarch.");
 			print_usage_line(2, "Examples:");
 			print_usage_line(2, "Examples:");
-			print_usage_line(3, "-target-features:atomics");
-			print_usage_line(3, "-target-features:\"sse2,aes\"");
-			print_usage_line(3, "-target-features:\"?\" for a list");
+				print_usage_line(3, "-target-features:atomics");
+				print_usage_line(3, "-target-features:\"sse2,aes\"");
+				print_usage_line(3, "-target-features:\"?\" for a list");
 		}
 		}
 	}
 	}
 
 
@@ -2810,11 +2844,11 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		if (print_flag("-vet")) {
 		if (print_flag("-vet")) {
 			print_usage_line(2, "Does extra checks on the code.");
 			print_usage_line(2, "Does extra checks on the code.");
 			print_usage_line(2, "Extra checks include:");
 			print_usage_line(2, "Extra checks include:");
-			print_usage_line(3, "-vet-unused");
-			print_usage_line(3, "-vet-unused-variables");
-			print_usage_line(3, "-vet-unused-imports");
-			print_usage_line(3, "-vet-shadowing");
-			print_usage_line(3, "-vet-using-stmt");
+				print_usage_line(3, "-vet-unused");
+				print_usage_line(3, "-vet-unused-variables");
+				print_usage_line(3, "-vet-unused-imports");
+				print_usage_line(3, "-vet-shadowing");
+				print_usage_line(3, "-vet-using-stmt");
 		}
 		}
 
 
 		if (print_flag("-vet-cast")) {
 		if (print_flag("-vet-cast")) {
@@ -3851,6 +3885,11 @@ end_of_code_gen:;
 		defer (gb_free(heap_allocator(), exe_name.text));
 		defer (gb_free(heap_allocator(), exe_name.text));
 
 
 		system_must_exec_command_line_app("odin run", "\"%.*s\" %.*s", LIT(exe_name), LIT(run_args_string));
 		system_must_exec_command_line_app("odin run", "\"%.*s\" %.*s", LIT(exe_name), LIT(run_args_string));
+
+		if (!build_context.keep_executable) {
+			char const *filename = cast(char const *)exe_name.text;
+			gb_file_remove(filename);
+		}
 	}
 	}
 	return 0;
 	return 0;
 }
 }

+ 6 - 4
tests/benchmark/text/regex/benchmark_regex.odin

@@ -103,9 +103,11 @@ expensive_for_backtrackers :: proc(t: ^testing.T) {
 
 
 @test
 @test
 global_capture_end_word :: proc(t: ^testing.T) {
 global_capture_end_word :: proc(t: ^testing.T) {
+	// NOTE: The previous behavior of `.Global`, which was to automatically
+	// insert `.*?` at the start of the pattern, is now default.
 	EXPR :: `Hellope World!`
 	EXPR :: `Hellope World!`
 
 
-	rex, err := regex.create(EXPR, { .Global })
+	rex, err := regex.create(EXPR, { /*.Global*/ })
 	if !testing.expect_value(t, err, nil) {
 	if !testing.expect_value(t, err, nil) {
 		return
 		return
 	}
 	}
@@ -145,7 +147,7 @@ global_capture_end_word_unicode :: proc(t: ^testing.T) {
 	EXPR :: `こにちは`
 	EXPR :: `こにちは`
 	needle := string(EXPR)
 	needle := string(EXPR)
 
 
-	rex, err := regex.create(EXPR, { .Global, .Unicode })
+	rex, err := regex.create(EXPR, { /*.Global,*/ .Unicode })
 	if !testing.expect_value(t, err, nil) {
 	if !testing.expect_value(t, err, nil) {
 		return
 		return
 	}
 	}
@@ -185,7 +187,7 @@ global_capture_end_word_unicode :: proc(t: ^testing.T) {
 alternations :: proc(t: ^testing.T) {
 alternations :: proc(t: ^testing.T) {
 	EXPR :: `a(?:bb|cc|dd|ee|ff)`
 	EXPR :: `a(?:bb|cc|dd|ee|ff)`
 
 
-	rex, err := regex.create(EXPR, { .No_Capture, .Global })
+	rex, err := regex.create(EXPR, { .No_Capture, /*.Global*/ })
 	if !testing.expect_value(t, err, nil) {
 	if !testing.expect_value(t, err, nil) {
 		return
 		return
 	}
 	}
@@ -219,7 +221,7 @@ classes :: proc(t: ^testing.T) {
 	EXPR :: `[\w\d]+`
 	EXPR :: `[\w\d]+`
 	NEEDLE :: "0123456789abcdef"
 	NEEDLE :: "0123456789abcdef"
 
 
-	rex, err := regex.create(EXPR, { .Global })
+	rex, err := regex.create(EXPR, { /*.Global*/ })
 	if !testing.expect_value(t, err, nil) {
 	if !testing.expect_value(t, err, nil) {
 		return
 		return
 	}
 	}

+ 174 - 19
tests/core/text/regex/test_core_text_regex.odin

@@ -51,13 +51,13 @@ check_expression_with_flags :: proc(t: ^testing.T, pattern: string, flags: regex
 }
 }
 
 
 check_expression :: proc(t: ^testing.T, pattern, haystack: string, needles: ..string, extra_flags := regex.Flags{}, loc := #caller_location) {
 check_expression :: proc(t: ^testing.T, pattern, haystack: string, needles: ..string, extra_flags := regex.Flags{}, loc := #caller_location) {
-	check_expression_with_flags(t, pattern, { .Global } + extra_flags,
+	check_expression_with_flags(t, pattern, extra_flags,
 		haystack, ..needles, loc = loc)
 		haystack, ..needles, loc = loc)
-	check_expression_with_flags(t, pattern, { .Global, .No_Optimization } + extra_flags,
+	check_expression_with_flags(t, pattern, { .No_Optimization } + extra_flags,
 		haystack, ..needles, loc = loc)
 		haystack, ..needles, loc = loc)
-	check_expression_with_flags(t, pattern, { .Global, .Unicode } + extra_flags,
+	check_expression_with_flags(t, pattern, { .Unicode } + extra_flags,
 		haystack, ..needles, loc = loc)
 		haystack, ..needles, loc = loc)
-	check_expression_with_flags(t, pattern, { .Global, .Unicode, .No_Optimization } + extra_flags,
+	check_expression_with_flags(t, pattern, { .Unicode, .No_Optimization } + extra_flags,
 		haystack, ..needles, loc = loc)
 		haystack, ..needles, loc = loc)
 }
 }
 
 
@@ -72,17 +72,18 @@ expect_error :: proc(t: ^testing.T, pattern: string, expected_error: typeid, fla
 	testing.expect_value(t, variant_ti, expected_ti, loc = loc)
 	testing.expect_value(t, variant_ti, expected_ti, loc = loc)
 }
 }
 
 
-check_capture :: proc(t: ^testing.T, got, expected: regex.Capture, loc := #caller_location) {
-	testing.expect_value(t, len(got.pos),    len(got.groups),      loc = loc)
-	testing.expect_value(t, len(got.pos),    len(expected.pos),    loc = loc)
-	testing.expect_value(t, len(got.groups), len(expected.groups), loc = loc)
+check_capture :: proc(t: ^testing.T, got, expected: regex.Capture, loc := #caller_location) -> (ok: bool) {
+	testing.expect_value(t, len(got.pos),    len(got.groups),      loc = loc) or_return
+	testing.expect_value(t, len(got.pos),    len(expected.pos),    loc = loc) or_return
+	testing.expect_value(t, len(got.groups), len(expected.groups), loc = loc) or_return
 
 
 	if len(got.pos) == len(expected.pos) {
 	if len(got.pos) == len(expected.pos) {
 		for i in 0..<len(got.pos) {
 		for i in 0..<len(got.pos) {
-			testing.expect_value(t, got.pos[i],    expected.pos[i],    loc = loc)
-			testing.expect_value(t, got.groups[i], expected.groups[i], loc = loc)
+			testing.expect_value(t, got.pos[i],    expected.pos[i],    loc = loc) or_return
+			testing.expect_value(t, got.groups[i], expected.groups[i], loc = loc) or_return
 		}
 		}
 	}
 	}
+	return true
 }
 }
 
 
 @test
 @test
@@ -212,6 +213,12 @@ test_alternation :: proc(t: ^testing.T) {
 	check_expression(t, EXPR, "cc", "cc")
 	check_expression(t, EXPR, "cc", "cc")
 }
 }
 
 
+@test
+test_alternation_order :: proc(t: ^testing.T) {
+	check_expression(t, "a|ab", "ab", "a")
+	check_expression(t, "ab|a", "ab", "ab")
+}
+
 @test
 @test
 test_optional :: proc(t: ^testing.T) {
 test_optional :: proc(t: ^testing.T) {
 	EXPR :: "a?a?a?aaa"
 	EXPR :: "a?a?a?aaa"
@@ -516,7 +523,7 @@ test_pos_index_explicitly :: proc(t: ^testing.T) {
 	STR :: "This is an island."
 	STR :: "This is an island."
 	EXPR :: `\bis\b`
 	EXPR :: `\bis\b`
 
 
-	rex, err := regex.create(EXPR, { .Global })
+	rex, err := regex.create(EXPR)
 	if !testing.expect_value(t, err, nil) {
 	if !testing.expect_value(t, err, nil) {
 		return
 		return
 	}
 	}
@@ -642,9 +649,9 @@ test_unicode_explicitly :: proc(t: ^testing.T) {
 	}
 	}
 	{
 	{
 		EXPR :: "こにちは!"
 		EXPR :: "こにちは!"
-		check_expression_with_flags(t, EXPR, { .Global, .Unicode },
+		check_expression_with_flags(t, EXPR, { .Unicode },
 			"Hello こにちは!", "こにちは!")
 			"Hello こにちは!", "こにちは!")
-		check_expression_with_flags(t, EXPR, { .Global, .Unicode, .No_Optimization },
+		check_expression_with_flags(t, EXPR, { .Unicode, .No_Optimization },
 			"Hello こにちは!", "こにちは!")
 			"Hello こにちは!", "こにちは!")
 	}
 	}
 }
 }
@@ -901,12 +908,12 @@ test_everything_at_once :: proc(t: ^testing.T) {
 @test
 @test
 test_creation_from_user_string :: proc(t: ^testing.T) {
 test_creation_from_user_string :: proc(t: ^testing.T) {
 	{
 	{
-		USER_EXPR :: `/^hellope$/gmixun-`
+		USER_EXPR :: `/^hellope$/mixun-`
 		STR :: "hellope"
 		STR :: "hellope"
 		rex, err := regex.create_by_user(USER_EXPR)
 		rex, err := regex.create_by_user(USER_EXPR)
 		defer regex.destroy(rex)
 		defer regex.destroy(rex)
 		testing.expect_value(t, err, nil)
 		testing.expect_value(t, err, nil)
-		testing.expect_value(t, rex.flags, regex.Flags{ .Global, .Multiline, .Case_Insensitive, .Ignore_Whitespace, .Unicode, .No_Capture, .No_Optimization })
+		testing.expect_value(t, rex.flags, regex.Flags{ .Multiline, .Case_Insensitive, .Ignore_Whitespace, .Unicode, .No_Capture, .No_Optimization })
 
 
 		_, ok := regex.match(rex, STR)
 		_, ok := regex.match(rex, STR)
 		testing.expectf(t, ok, "expected user-provided RegEx %v to match %q", rex, STR)
 		testing.expectf(t, ok, "expected user-provided RegEx %v to match %q", rex, STR)
@@ -1102,24 +1109,121 @@ Iterator_Test :: struct {
 
 
 iterator_vectors := []Iterator_Test{
 iterator_vectors := []Iterator_Test{
 	{
 	{
-		`xxab32ab52xx`, `(ab\d{1})`, {}, // {.Global} implicitly added by the iterator
+		`xxab32ab52xx`, `(ab\d{1})`, {},
 		{
 		{
 			{pos = {{2, 5}, {2, 5}}, groups = {"ab3", "ab3"}},
 			{pos = {{2, 5}, {2, 5}}, groups = {"ab3", "ab3"}},
 			{pos = {{6, 9}, {6, 9}}, groups = {"ab5", "ab5"}},
 			{pos = {{6, 9}, {6, 9}}, groups = {"ab5", "ab5"}},
 		},
 		},
 	},
 	},
 	{
 	{
-		`xxfoobarxfoobarxx`, `f(o)ob(ar)`, {.Global},
+		`xxfoobarxfoobarxx`, `f(o)ob(ar)`, {},
 		{
 		{
 			{pos = {{2,  8},  {3,  4},  {6,  8}}, groups = {"foobar", "o", "ar"}},
 			{pos = {{2,  8},  {3,  4},  {6,  8}}, groups = {"foobar", "o", "ar"}},
 			{pos = {{9, 15}, {10, 11}, {13, 15}}, groups = {"foobar", "o", "ar"}},
 			{pos = {{9, 15}, {10, 11}, {13, 15}}, groups = {"foobar", "o", "ar"}},
 		},
 		},
 	},
 	},
+	{
+		`aaa`, `a`, {},
+		{
+			{pos = {{0,  1}}, groups = {"a"}},
+			{pos = {{1,  2}}, groups = {"a"}},
+			{pos = {{2,  3}}, groups = {"a"}},
+		},
+	},
+	{
+		`aaa`, `\w`, {},
+		{
+			{pos = {{0,  1}}, groups = {"a"}},
+			{pos = {{1,  2}}, groups = {"a"}},
+			{pos = {{2,  3}}, groups = {"a"}},
+		},
+	},
+	{
+		`aかか`, `.`, {.Unicode},
+		{
+			{pos = {{0,  1}}, groups = {"a"}},
+			{pos = {{1,  4}}, groups = {"か"}},
+			{pos = {{4,  7}}, groups = {"か"}},
+		},
+	},
+	{
+		`ゆめ.`, `.`, {.Unicode},
+		{
+			{pos = {{0,  3}}, groups = {"ゆ"}},
+			{pos = {{3,  6}}, groups = {"め"}},
+			{pos = {{6,  7}}, groups = {"."}},
+		},
+	},
+	// This pattern is not anchored, so this is valid per the new behavior.
+	{
+		`ababa`, `(?:a|ab)`, {},
+		{
+			{pos = {{0,  1}}, groups = {"a"}},
+			{pos = {{2,  3}}, groups = {"a"}},
+			{pos = {{4,  5}}, groups = {"a"}},
+		},
+	},
+	// Ensure alternation follows expected ordering of left-higher priority.
+	{
+		`ababa`, `(?:ab|a)`, {},
+		{
+			{pos = {{0,  2}}, groups = {"ab"}},
+			{pos = {{2,  4}}, groups = {"ab"}},
+			{pos = {{4,  5}}, groups = {"a"}},
+		},
+	},
+	// This one is anchored, so only one match.
+	//
+	// This test ensures the behavior of `Assert_Start` is checking against the
+	// start of the string and we haven't just slid the memory string itself.
+	{
+		`ababa`, `^(?:a|b)`, {},
+		{
+			{pos = {{0,  1}}, groups = {"a"}},
+		},
+	},
+	{
+		`ababa`, `a$`, {},
+		{
+			{pos = {{4,  5}}, groups = {"a"}},
+		},
+	},
+	// A blank pattern on a blank string is valid and must not loop infinitely.
+	{
+		``, ``, {},
+		{
+			{pos = {{0,  0}}, groups = {""}},
+		},
+	},
+	// These too are valid.
+	//
+	// The iterator must bail out when it encounters any situation which would
+	// loop infinitely, but only after giving at least one valid match if one
+	// exists, as this would accord with attempting to singularly match the
+	// pattern against the string and provide a positive result.
+	{
+		`a`, ``, {},
+		{
+			{pos = {{0,  0}}, groups = {""}},
+		},
+	},
+	{
+		``, `$`, {},
+		{
+			{pos = {{0,  0}}, groups = {""}},
+		},
+	},
+	{
+		`aaa`, `$`, {},
+		{
+			{pos = {{3,  3}}, groups = {""}},
+		},
+	},
 }
 }
 
 
 @test
 @test
 test_match_iterator :: proc(t: ^testing.T) {
 test_match_iterator :: proc(t: ^testing.T) {
-	for test in iterator_vectors {
+	vector: for test in iterator_vectors {
 		it, err := regex.create_iterator(test.haystack, test.pattern, test.flags)
 		it, err := regex.create_iterator(test.haystack, test.pattern, test.flags)
 		defer regex.destroy(it)
 		defer regex.destroy(it)
 
 
@@ -1128,10 +1232,61 @@ test_match_iterator :: proc(t: ^testing.T) {
 
 
 		for capture, idx in regex.match(&it) {
 		for capture, idx in regex.match(&it) {
 			if idx >= len(test.expected) {
 			if idx >= len(test.expected) {
+				log.errorf("got more than expected number of captures for matching string %q against pattern %q\n\tidx %i = %v", test.haystack, test.pattern, idx, capture)
+				continue vector
+			}
+			if !check_capture(t, capture, test.expected[idx]) {
+				log.errorf("capture check failed on string %q against pattern %q", test.haystack, test.pattern)
+			}
+		}
+		testing.expectf(t, it.idx == len(test.expected), "expected idx to be %i, got %i on string %q against pattern %q", len(test.expected), it.idx, test.haystack, test.pattern)
+	}
+}
+
+@test
+test_iterator_reset :: proc(t: ^testing.T) {
+	test := Iterator_Test{
+		`aaa`, `a`, {},
+		{
+			{pos = {{0,  1}}, groups = {"a"}},
+			{pos = {{1,  2}}, groups = {"a"}},
+			{pos = {{2,  3}}, groups = {"a"}},
+		},
+	}
+
+	out: {
+		it, err := regex.create_iterator(`aaa`, `a`)
+		defer regex.destroy(it)
+
+		testing.expect_value(t, err, nil)
+		(err == nil) or_break out
+
+		for capture, idx in regex.match(&it) {
+			if idx >= len(test.expected) {
+				log.errorf("got more than expected number of captures for matching string %q against pattern %q\n\tidx %i = %v", test.haystack, test.pattern, idx, capture)
 				break
 				break
 			}
 			}
 			check_capture(t, capture, test.expected[idx])
 			check_capture(t, capture, test.expected[idx])
 		}
 		}
 		testing.expect_value(t, it.idx, len(test.expected))
 		testing.expect_value(t, it.idx, len(test.expected))
+
+		// Do it again.
+		iterations := 0
+		regex.reset(&it)
+
+		// Mind that this loop can do nothing if it wasn't reset but leave us
+		// with the expected `idx` state.
+		//
+		// That's why we count iterations this time around.
+		for capture, idx in regex.match(&it) {
+			iterations += 1
+			if idx >= len(test.expected) {
+				log.errorf("got more than expected number of captures for matching string %q against pattern %q\n\tidx %i = %v", test.haystack, test.pattern, idx, capture)
+				break
+			}
+			check_capture(t, capture, test.expected[idx])
+		}
+		testing.expect_value(t, it.idx, len(test.expected))
+		testing.expect_value(t, iterations, len(test.expected))
 	}
 	}
-}
+}

+ 1 - 1
vendor/sdl2/sdl_rwops.odin

@@ -87,7 +87,7 @@ foreign lib {
 	RWseek  :: proc(ctx: ^RWops, offset: i64, whence: c.int) -> i64 ---
 	RWseek  :: proc(ctx: ^RWops, offset: i64, whence: c.int) -> i64 ---
 	RWtell  :: proc(ctx: ^RWops) -> i64 ---
 	RWtell  :: proc(ctx: ^RWops) -> i64 ---
 	RWread  :: proc(ctx: ^RWops, ptr: rawptr, size: c.size_t, maxnum: c.size_t) -> c.size_t ---
 	RWread  :: proc(ctx: ^RWops, ptr: rawptr, size: c.size_t, maxnum: c.size_t) -> c.size_t ---
-	RWwrite :: proc(ctx: ^RWops, size: c.size_t, num: c.size_t) -> c.size_t ---
+	RWwrite :: proc(ctx: ^RWops, ptr: rawptr, size: c.size_t, num: c.size_t) -> c.size_t ---
 	RWclose :: proc(ctx: ^RWops) -> c.int ---
 	RWclose :: proc(ctx: ^RWops) -> c.int ---
 
 
 	LoadFile_RW :: proc(src: ^RWops, datasize: ^c.size_t, freesrc: bool) -> rawptr ---
 	LoadFile_RW :: proc(src: ^RWops, datasize: ^c.size_t, freesrc: bool) -> rawptr ---

+ 12 - 4
vendor/sdl3/sdl3_mutex.odin

@@ -1,8 +1,8 @@
 package sdl3
 package sdl3
 
 
-Mutex  :: struct {}
-RWLock :: struct {}
-
+Mutex     :: struct {}
+RWLock    :: struct {}
+Semaphore :: struct {}
 
 
 @(default_calling_convention="c", link_prefix="SDL_", require_results)
 @(default_calling_convention="c", link_prefix="SDL_", require_results)
 foreign lib {
 foreign lib {
@@ -19,4 +19,12 @@ foreign lib {
 	TryLockRWLockForWriting :: proc(rwlock: ^RWLock) -> bool ---
 	TryLockRWLockForWriting :: proc(rwlock: ^RWLock) -> bool ---
 	UnlockRWLock            :: proc(rwlock: ^RWLock) ---
 	UnlockRWLock            :: proc(rwlock: ^RWLock) ---
 	DestroyRWLock           :: proc(rwlock: ^RWLock) ---
 	DestroyRWLock           :: proc(rwlock: ^RWLock) ---
-}
+
+	CreateSemaphore         :: proc(initial_value: Uint32) -> ^Semaphore ---
+	DestroySemaphore        :: proc(sem: ^Semaphore) ---
+	GetSemaphoreValue       :: proc(sem: ^Semaphore) -> Uint32 ---
+	SignalSemaphore         :: proc(sem: ^Semaphore) ---
+	TryWaitSemaphore        :: proc(sem: ^Semaphore) -> bool ---
+	WaitSemaphore           :: proc(sem: ^Semaphore) ---
+	WaitSemaphoreTimeout    :: proc(sem: ^Semaphore, timeout_ms: Sint32) ---
+}

+ 1 - 1
vendor/wgpu/wgpu.js

@@ -2033,7 +2033,7 @@ class WebGPUInterface {
 						addressModeW:  this.enumeration("AddressMode", off(4)),
 						addressModeW:  this.enumeration("AddressMode", off(4)),
 						magFilter:     this.enumeration("FilterMode", off(4)),
 						magFilter:     this.enumeration("FilterMode", off(4)),
 						minFilter:     this.enumeration("FilterMode", off(4)),
 						minFilter:     this.enumeration("FilterMode", off(4)),
-						mipMapFilter:  this.enumeration("MipmapFilterMode", off(4)),
+						mipmapFilter:  this.enumeration("MipmapFilterMode", off(4)),
 						lodMinClamp:   this.mem.loadF32(off(4)),
 						lodMinClamp:   this.mem.loadF32(off(4)),
 						lodMaxClamp:   this.mem.loadF32(off(4)),
 						lodMaxClamp:   this.mem.loadF32(off(4)),
 						compare:       this.enumeration("CompareFunction", off(4)),
 						compare:       this.enumeration("CompareFunction", off(4)),