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

This commit is contained in:
Vincent Billet
2025-05-26 08:16:45 +02:00
committed by GitHub
49 changed files with 978 additions and 322 deletions
@@ -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
View File
@@ -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
}
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
-21
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+4
View File
@@ -0,0 +1,4 @@
/*
This package is for interacting with the command line interface of the system.
*/
package terminal
+87
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+93 -69
View File
@@ -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
// 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)
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)
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
}
-36
View File
@@ -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
View File
@@ -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
-3
View File
@@ -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
View File
@@ -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
View File
@@ -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] = {}
}
}
/*
@@ -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
View File
@@ -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 == '\\' {