瀏覽代碼

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

Vincent Billet 3 月之前
父節點
當前提交
bb274ab512
共有 49 個文件被更改,包括 979 次插入323 次删除
  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) {
 	n := builtin.len(pq.queue)
 	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
 }

+ 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.
 is_supported :: proc "contextless" () -> bool {
-	features, ok := info.cpu_features.?
+	features, ok := info.cpu.features.?
 	if !ok {
 		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}
 		}
 
-		features, ok := info.cpu_features.?
+		features, ok := info.cpu.features.?
 		if !ok {
 			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 {
 	req_features :: info.CPU_Features{.avx, .avx2}
 
-	features, ok := info.cpu_features.?
+	features, ok := info.cpu.features.?
 	if !ok {
 		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
 // SHA-224/SHA-256 is supported.
 is_hardware_accelerated_256 :: proc "contextless" () -> bool {
-	features, ok := info.cpu_features.?
+	features, ok := info.cpu.features.?
 	if !ok {
 		return false
 	}

+ 53 - 11
core/log/file_console_logger.odin

@@ -2,10 +2,12 @@
 #+build !orca
 package log
 
-import "core:encoding/ansi"
+import "base:runtime"
 import "core:fmt"
 import "core:strings"
 import "core:os"
+import "core:terminal"
+import "core:terminal/ansi"
 import "core:time"
 
 Level_Headers := [?]string{
@@ -37,11 +39,36 @@ File_Console_Logger_Data :: struct {
 	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 {
 	data := new(File_Console_Logger_Data, allocator)
 	data.file_handle = h
 	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) {
@@ -56,19 +83,15 @@ create_console_logger :: proc(lowest := Level.Debug, opt := Default_Console_Logg
 	data := new(File_Console_Logger_Data, allocator)
 	data.file_handle = os.INVALID_HANDLE
 	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) {
 	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.
 	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())
 	}
 
-	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
 	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) {
 
 	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 - '+'
-		if RADIX_TABLE_REVERSE_SIZE <= pos {
+		if RADIX_TABLE_REVERSE_SIZE <= u32(pos) {
 			break
 		}
 		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)
-block_mark_as_used :: proc(block: ^Block_Header, ) {
+block_mark_as_used :: proc(block: ^Block_Header) {
 	next := block_next(block)
 	block_set_prev_used(next)
 	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
 }
+
+_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
 }
+
+_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.
 
-import "base:runtime"
-import "core:strings"
-import "core:sys/posix"
-
 _are_paths_identical :: proc(a, b: string) -> (identical: bool) {
 	return a == b
 }
@@ -26,23 +22,6 @@ _is_absolute_path :: proc(path: string) -> bool {
 	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 {
 	base_rooted   := len(base)   > 0 && _is_path_separator(base[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 }
 
 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

+ 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: 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)
 cpu_name_buf: [128]byte
@@ -53,7 +57,7 @@ init_cpu_name :: proc "contextless" () {
 
 	when ODIN_OS == .Darwin {
 		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
 		}
 	}
@@ -61,10 +65,10 @@ init_cpu_name :: proc "contextless" () {
 	if generic {
 		when ODIN_ARCH == .arm64 {
 			copy(cpu_name_buf[:], "ARM64")
-			cpu_name = string(cpu_name_buf[:len("ARM64")])
+			cpu.name = string(cpu_name_buf[:len("ARM64")])
 		} else {
 			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_cpu_features :: proc "contextless" () {
 	@(static) features: CPU_Features
-	defer cpu_features = features
+	defer cpu.features = features
 
 	try_set :: proc "contextless" (name: cstring, feature: CPU_Feature) -> (ok: bool) {
 		support: b32

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

@@ -3,12 +3,6 @@ package sysinfo
 
 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 {
 	aes,       // AES hardware implementation (AES NI)
 	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: 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_cpu_features :: proc "c" () {
@@ -88,7 +86,7 @@ init_cpu_features :: proc "c" () {
 	when ODIN_OS == .FreeBSD || ODIN_OS == .OpenBSD || ODIN_OS == .NetBSD {
 		// xgetbv is an illegal instruction under FreeBSD 13, OpenBSD 7.1 and NetBSD 10
 		// return before probing further
-		cpu_features = set
+		cpu.features = set
 		return
 	}
 
@@ -151,7 +149,7 @@ init_cpu_features :: proc "c" () {
 	try_set(&set, .rdseed, 18, ebx7)
 	try_set(&set, .adx,    19, ebx7)
 
-	cpu_features = set
+	cpu.features = set
 }
 
 @(private)
@@ -179,5 +177,11 @@ init_cpu_name :: proc "c" () {
 	for len(brand) > 0 && brand[len(brand) - 1] == 0 || 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 }
 
 	features: CPU_Features
-	defer cpu_features = features
+	defer cpu.features = features
 
 	str := string(buf[:n])
 	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_cpu_features :: proc() {
 	_features: CPU_Features
-	defer cpu_features = _features
+	defer cpu.features = _features
 
 	HWCAP_Bits :: enum u64 {
 		I = 'I' - 'A',
@@ -109,5 +109,5 @@ init_cpu_features :: proc() {
 
 @(init, private)
 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: 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"
 
 	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.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
 HighMemoryResourceNotification :: MEMORY_RESOURCE_NOTIFICATION_TYPE.HighMemoryResourceNotification
 
-
 @(default_calling_convention="system")
 foreign kernel32 {
 	CreateMemoryResourceNotification :: proc(
@@ -1194,7 +1193,7 @@ DUMMYUNIONNAME_u :: struct #raw_union {
 SYSTEM_LOGICAL_PROCESSOR_INFORMATION :: struct {
 	ProcessorMask: ULONG_PTR,
 	Relationship: LOGICAL_PROCESSOR_RELATIONSHIP,
-	DummyUnion: DUMMYUNIONNAME_u,
+	using DummyUnion: DUMMYUNIONNAME_u,
 }
 
 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 "core:encoding/ansi"
 import "core:fmt"
 import "core:io"
 import "core:mem"
 import "core:path/filepath"
 import "core:strings"
+import "core:terminal/ansi"
 
 // Definitions of colors for use in the test runner.
 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:runtime"
 import "core:bytes"
-import "core:encoding/ansi"
 @require import "core:encoding/base64"
 @require import "core:encoding/json"
 import "core:fmt"
@@ -25,6 +24,8 @@ import "core:os"
 import "core:slice"
 @require import "core:strings"
 import "core:sync/chan"
+import "core:terminal"
+import "core:terminal/ansi"
 import "core:thread"
 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,...`
 TEST_NAMES            : string : #config(ODIN_TEST_NAMES, "")
 // 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)
 // Copy failed tests to the clipboard when done.
 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 {
 	total:    int,
 	success:  int,
@@ -129,11 +134,16 @@ run_test_task :: proc(task: thread.Task) {
 	
 	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 = {
 		procedure = test_logger_proc,
 		data = &data.t,
 		lowest_level = get_log_level(),
-		options = Default_Test_Logger_Opts,
+		options = logger_options,
 	}
 
 	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))
 	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.
 
 	alloc_error: mem.Allocator_Error
@@ -268,12 +281,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	total_done_count    := 0
 	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 {
 		// 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)
 	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, 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.
 	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 = {
 		procedure = runner_logger_proc,
 		data = &log_messages,
 		lowest_level = get_log_level(),
-		options = Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure},
+		options = logger_options,
 	}
 
 	run_index: int
@@ -481,11 +499,13 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 
 	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)
 		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
 		}
 
-		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 {
 			#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)
 				}
 
-				when FANCY_OUTPUT {
+				if should_show_animations {
 					bypass_progress_overwrite = true
 					signals_were_raised = true
 				}
@@ -766,7 +786,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 
 		// -- Redraw.
 
-		when FANCY_OUTPUT {
+		if should_show_animations {
 			if len(log_messages) == 0 && !needs_to_redraw(report) {
 				continue main_loop
 			}
@@ -776,7 +796,9 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 			}
 		} else {
 			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
 			}
 
@@ -801,7 +823,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 		clear(&log_messages)
 		bytes.buffer_reset(&batch_buffer)
 
-		when FANCY_OUTPUT {
+		if should_show_animations {
 			redraw_report(batch_writer, report)
 			draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count)
 			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)
 
-	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
 		// bar in plain mode.
 		fmt.wprintln(batch_writer)
@@ -836,24 +858,28 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	
 	if total_done_count != total_test_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,
-			" " + SGR_READY + "%i" + SGR_RESET + " %s left undone.",
+			message,
 			not_run_count,
 			"test was" if not_run_count == 1 else "tests were")
 	}
 
 	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,
-			" %s " + SGR_SUCCESS + "successful." + SGR_RESET,
+			message,
 			"The test was" if total_test_count == 1 else "All tests were")
 	} else if total_failure_count > 0 {
 		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,
-				" %s " + SGR_FAILED + "failed." + SGR_RESET,
+				message,
 				"The test" if total_test_count == 1 else "All tests")
 		} else {
+			message := " %i test%s failed." if global_log_colors_disabled else " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed."
 			fmt.wprintfln(batch_writer,
-				" " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed.",
+				message,
 				total_failure_count,
 				"" 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 {
 			fmt.wprintln(batch_writer, `
 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)
 	}
 
-	when ODIN_OS == .Windows {
-		console_ansi_fini()
-	}
-
 	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 "core:c/libc"
-import "core:encoding/ansi"
-import "core:sync"
 import "core:os"
+import "core:sync"
+import "core:terminal/ansi"
 
 @(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
 		// undefined behavior in C99 but possibly tolerated in POSIX 2008.
 		// 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.
 		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))
 
 Flag :: enum u8 {
-	// Global: try to match the pattern anywhere in the string.
-	Global,
 	// Multiline: treat `^` and `$` as if they also match newlines.
 	Multiline,
 	// Case Insensitive: treat `a-z` as if it was also `A-Z`.
@@ -36,7 +34,6 @@ Flags :: bit_set[Flag; u8]
 
 @(rodata)
 Flag_To_Letter := #sparse[Flag]u8 {
-	.Global            = 'g',
 	.Multiline         = 'm',
 	.Case_Insensitive  = 'i',
 	.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
 
-	add_global: if .Global in flags {
+	optimize_opening: {
 		// Check if the opening to the pattern is predictable.
 		// If so, use one of the optimized Wait opcodes.
 		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)
 				inject_at(&code, pc_open, Opcode(code[pc + size_of(Opcode) + pc_open]))
 				pc_open += size_of(u8)
-				break add_global
+				break optimize_opening
 
 			case .Rune:
 				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)
 				inject_raw(&code, pc_open, operand)
 				pc_open += size_of(rune)
-				break add_global
+				break optimize_opening
 
 			case .Rune_Class:
 				inject_at(&code, pc_open, Opcode.Wait_For_Rune_Class)
 				pc_open += size_of(Opcode)
 				inject_at(&code, pc_open, Opcode(code[pc + size_of(Opcode) + pc_open]))
 				pc_open += size_of(u8)
-				break add_global
+				break optimize_opening
 
 			case .Rune_Class_Negated:
 				inject_at(&code, pc_open, Opcode.Wait_For_Rune_Class_Negated)
 				pc_open += size_of(Opcode)
 				inject_at(&code, pc_open, Opcode(code[pc + size_of(Opcode) + pc_open]))
 				pc_open += size_of(u8)
-				break add_global
+				break optimize_opening
 
 			case .Save:
 				continue
+
+			case .Assert_Start:
+				break optimize_opening
+
 			case:
 				break seek_loop
 			}

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

@@ -77,6 +77,8 @@ Match_Iterator :: struct {
 	vm:       virtual_machine.Machine,
 	idx:      int,
 	temp:     runtime.Allocator,
+	threads:  int,
+	done:     bool,
 }
 
 /*
@@ -101,7 +103,6 @@ create :: proc(
 	permanent_allocator := context.allocator,
 	temporary_allocator := context.temp_allocator,
 ) -> (result: Regular_Expression, err: Error) {
-
 	// For the sake of speed and simplicity, we first run all the intermediate
 	// processes such as parsing and compilation through the temporary
 	// 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:
 
-- 'g': Global
 - 'm': Multiline
 - 'i': Case_Insensitive
 - 'x': Ignore_Whitespace
@@ -243,7 +243,6 @@ create_by_user :: proc(
 	// to `end` here.
 	for r in pattern[start + end:] {
 		switch r {
-		case 'g': flags += { .Global }
 		case 'm': flags += { .Multiline }
 		case 'i': flags += { .Case_Insensitive }
 		case 'x': flags += { .Ignore_Whitespace }
@@ -282,8 +281,6 @@ create_iterator :: proc(
 	permanent_allocator := context.allocator,
 	temporary_allocator := context.temp_allocator,
 ) -> (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 {
 		return {}, .Unsupported_Flag
@@ -294,6 +291,7 @@ create_iterator :: proc(
 	result.temp          = temporary_allocator
 	result.vm            = virtual_machine.create(result.regex.program, str)
 	result.vm.class_data = result.regex.class_data
+	result.threads       = max(1, virtual_machine.opcode_count(result.vm.code) - 1)
 
 	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,
 		"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()
 
+	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
 	{
 		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)
 	num_groups: int
 
@@ -488,9 +527,7 @@ match_iterator :: proc(it: ^Match_Iterator) -> (result: Capture, index: int, ok:
 		num_groups = n
 	}
 
-	defer if ok {
-		it.idx += 1
-	}
+	defer it.idx += 1
 
 	if num_groups > 0 {
 		result = {it.capture.pos[:num_groups], it.capture.groups[:num_groups]}
@@ -504,8 +541,24 @@ match :: proc {
 	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) {
-	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 {
 	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 {
 		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
 		}
 	}
@@ -652,4 +652,4 @@ destroy :: proc(vm: Machine, allocator := context.allocator) {
 	delete(vm.busy_map)
 	free(vm.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 'z': return "dozenal literal"
 		case 'x': return "hexadecimal literal"
+		case 'h': return "hexadecimal literal"
 		}
 		return "decimal literal"
 	}
@@ -360,7 +361,8 @@ scan_number :: proc(s: ^Scanner, ch: rune, seen_dot: bool) -> (rune, rune) {
 					base, prefix = 12, 'z'
 				case 'h':
 					tok = Float
-					fallthrough
+					ch = advance(s)
+					base, prefix = 16, 'h'
 				case 'x':
 					ch = advance(s)
 					base, prefix = 16, 'x'
@@ -447,7 +449,7 @@ scan_string :: proc(s: ^Scanner, quote: rune) -> (n: int) {
 	ch := advance(s)
 	for ch != quote {
 		if ch == '\n' || ch < 0 {
-			error(s, "literal no terminated")
+			error(s, "literal not terminated")
 			return
 		}
 		if ch == '\\' {

+ 5 - 2
examples/all/all_main.odin

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

+ 33 - 9
src/build_settings.cpp

@@ -441,6 +441,7 @@ struct BuildContext {
 	String extra_assembler_flags;
 	String microarch;
 	BuildModeKind build_mode;
+	bool   keep_executable;
 	bool   generate_docs;
 	bool   custom_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] == '\\')) {
 				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_extension_from_path(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
 			// Turn the trailing folder into the file name
 			if (output_path.name.len == 0) {
@@ -2278,9 +2302,10 @@ gb_internal bool init_build_paths(String init_filename) {
 		case TargetOs_windows:
 		case TargetOs_linux:
 		case TargetOs_darwin:
+		case TargetOs_freebsd:
 			break;
 		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;
 		}
 	}
@@ -2288,12 +2313,10 @@ gb_internal bool init_build_paths(String init_filename) {
 	if (build_context.sanitizer_flags & SanitizerFlag_Memory) {
 		switch (build_context.metrics.os) {
 		case TargetOs_linux:
+		case TargetOs_freebsd:
 			break;
 		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;
 		}
 	}
@@ -2302,9 +2325,10 @@ gb_internal bool init_build_paths(String init_filename) {
 		switch (build_context.metrics.os) {
 		case TargetOs_linux:
 		case TargetOs_darwin:
+		case TargetOs_freebsd:
 			break;
 		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;
 		}
 	}

+ 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 &&
-	    (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;
 		if (1 < field_name.len && field_name.len <= 4) {
 			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 *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);
 
@@ -5558,10 +5568,6 @@ gb_internal Entity *check_selector(CheckerContext *c, Operand *operand, Ast *nod
 				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);
 			add_type_and_value(c, operand->expr, operand->mode, operand->type, operand->value);
 			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 ((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_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
 					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) {
 				// 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 ");

+ 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, "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_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, "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);
 
 	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.metrics.os == TargetOs_windows) {
+		switch (build_context.metrics.os) {
+		case TargetOs_windows: {
 			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"));
 			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"));
 			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) {
 				build_context.extra_linker_flags = str_lit("-fsanitize=address");
 			} else {
 				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.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) {
 				build_context.extra_linker_flags = str_lit("-fsanitize=memory");
 			} else {
 				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.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) {
 				build_context.extra_linker_flags = str_lit("-fsanitize=thread");
 			} else {
 				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_Define,
 	BuildFlag_BuildMode,
+	BuildFlag_KeepExecutable,
 	BuildFlag_Target,
 	BuildFlag_Subtarget,
 	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_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_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_Subtarget,               str_lit("subtarget"),                 BuildFlagParam_String,  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;
 						}
+						case BuildFlag_KeepExecutable:
+							build_context.keep_executable = true;
+							break;
 
 						case BuildFlag_Debug:
 							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>")) {
 			print_usage_line(2, "Sets the build mode.");
 			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, "Example: -collection:shared=dir/to/shared");
 			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>")) {
 			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, "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, "Example: -define:SPAM=123");
 			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 (print_flag("-error-pos-style:<string>")) {
 			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>")) {
@@ -2466,8 +2477,8 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		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, "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>")) {
@@ -2478,8 +2489,8 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		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, "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>")) {
@@ -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 (print_flag("-linker:<string>")) {
 			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>")) {
 			print_usage_line(2, "Specifies the specific micro-architecture for the build in a string.");
 			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>")) {
 			print_usage_line(2, "Sets the optimization mode for compilation.");
 			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) {
 				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>")) {
 			print_usage_line(2, "Specifies the reloc mode.");
 			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)
@@ -2700,9 +2719,9 @@ gb_internal int print_show_help(String const arg0, String command, String option
 		if (print_flag("-sanitize:<string>")) {
 			print_usage_line(2, "Enables sanitization analysis.");
 			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.");
 		}
 	}
@@ -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, "Defines the subsystem for the application.");
 			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
+	}
 
+	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>")) {
 			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(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")) {
 			print_usage_line(2, "Does extra checks on the code.");
 			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")) {
@@ -3851,6 +3885,11 @@ end_of_code_gen:;
 		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));
+
+		if (!build_context.keep_executable) {
+			char const *filename = cast(char const *)exe_name.text;
+			gb_file_remove(filename);
+		}
 	}
 	return 0;
 }

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

@@ -103,9 +103,11 @@ expensive_for_backtrackers :: proc(t: ^testing.T) {
 
 @test
 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!`
 
-	rex, err := regex.create(EXPR, { .Global })
+	rex, err := regex.create(EXPR, { /*.Global*/ })
 	if !testing.expect_value(t, err, nil) {
 		return
 	}
@@ -145,7 +147,7 @@ global_capture_end_word_unicode :: proc(t: ^testing.T) {
 	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) {
 		return
 	}
@@ -185,7 +187,7 @@ global_capture_end_word_unicode :: proc(t: ^testing.T) {
 alternations :: proc(t: ^testing.T) {
 	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) {
 		return
 	}
@@ -219,7 +221,7 @@ classes :: proc(t: ^testing.T) {
 	EXPR :: `[\w\d]+`
 	NEEDLE :: "0123456789abcdef"
 
-	rex, err := regex.create(EXPR, { .Global })
+	rex, err := regex.create(EXPR, { /*.Global*/ })
 	if !testing.expect_value(t, err, nil) {
 		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_with_flags(t, pattern, { .Global } + extra_flags,
+	check_expression_with_flags(t, pattern, extra_flags,
 		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)
-	check_expression_with_flags(t, pattern, { .Global, .Unicode } + extra_flags,
+	check_expression_with_flags(t, pattern, { .Unicode } + extra_flags,
 		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)
 }
 
@@ -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)
 }
 
-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) {
 		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
@@ -212,6 +213,12 @@ test_alternation :: proc(t: ^testing.T) {
 	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_optional :: proc(t: ^testing.T) {
 	EXPR :: "a?a?a?aaa"
@@ -516,7 +523,7 @@ test_pos_index_explicitly :: proc(t: ^testing.T) {
 	STR :: "This is an island."
 	EXPR :: `\bis\b`
 
-	rex, err := regex.create(EXPR, { .Global })
+	rex, err := regex.create(EXPR)
 	if !testing.expect_value(t, err, nil) {
 		return
 	}
@@ -642,9 +649,9 @@ test_unicode_explicitly :: proc(t: ^testing.T) {
 	}
 	{
 		EXPR :: "こにちは!"
-		check_expression_with_flags(t, EXPR, { .Global, .Unicode },
+		check_expression_with_flags(t, EXPR, { .Unicode },
 			"Hello こにちは!", "こにちは!")
-		check_expression_with_flags(t, EXPR, { .Global, .Unicode, .No_Optimization },
+		check_expression_with_flags(t, EXPR, { .Unicode, .No_Optimization },
 			"Hello こにちは!", "こにちは!")
 	}
 }
@@ -901,12 +908,12 @@ test_everything_at_once :: proc(t: ^testing.T) {
 @test
 test_creation_from_user_string :: proc(t: ^testing.T) {
 	{
-		USER_EXPR :: `/^hellope$/gmixun-`
+		USER_EXPR :: `/^hellope$/mixun-`
 		STR :: "hellope"
 		rex, err := regex.create_by_user(USER_EXPR)
 		defer regex.destroy(rex)
 		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)
 		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{
 	{
-		`xxab32ab52xx`, `(ab\d{1})`, {}, // {.Global} implicitly added by the iterator
+		`xxab32ab52xx`, `(ab\d{1})`, {},
 		{
 			{pos = {{2, 5}, {2, 5}}, groups = {"ab3", "ab3"}},
 			{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 = {{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_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)
 		defer regex.destroy(it)
 
@@ -1128,10 +1232,61 @@ test_match_iterator :: proc(t: ^testing.T) {
 
 		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)
+				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
 			}
 			check_capture(t, capture, test.expected[idx])
 		}
 		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 ---
 	RWtell  :: proc(ctx: ^RWops) -> i64 ---
 	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 ---
 
 	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
 
-Mutex  :: struct {}
-RWLock :: struct {}
-
+Mutex     :: struct {}
+RWLock    :: struct {}
+Semaphore :: struct {}
 
 @(default_calling_convention="c", link_prefix="SDL_", require_results)
 foreign lib {
@@ -19,4 +19,12 @@ foreign lib {
 	TryLockRWLockForWriting :: proc(rwlock: ^RWLock) -> bool ---
 	UnlockRWLock            :: 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)),
 						magFilter:     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)),
 						lodMaxClamp:   this.mem.loadF32(off(4)),
 						compare:       this.enumeration("CompareFunction", off(4)),