Merge branch 'master' into bill/asm-cfg

This commit is contained in:
gingerBill
2026-08-25 15:04:39 +01:00
22 changed files with 488 additions and 296 deletions

View File

@@ -561,7 +561,7 @@ uint_max :: proc(n: uint, gen := context.random_generator) -> (val: uint) {
Generates a random unsigned 32 bit value in the range `[lo, hi)` using the provided random number generator. If no generator is provided the global random number generator will be used.
Inputs:
- lo: The lower bound of the generated number, this value is inclusice
- lo: The lower bound of the generated number, this value is inclusive
- hi: The upper bound of the generated number, this value is exclusive
Returns:

View File

@@ -19,6 +19,38 @@ import win "core:sys/windows"
@(private="package")
_FULLY_SUPPORTED :: true
// Poll is driven by AFD, the socket driver underneath winsock.
// `WSAEventSelect` is edge triggered (`FD_WRITE` is only recorded again after a
// send fails with WOULDBLOCK) and neither `select` nor `WSAPoll` reports send
// buffer space, so neither can give the level triggered readiness `poll` promises.
// AFD also completes on the IOCP, which makes a poll an ordinary overlapped operation.
IOCTL_AFD_POLL :: 0x00012024
SIO_BASE_HANDLE :: win.DWORD(0x48000022)
AFD_POLL_RECEIVE :: 0x0001
AFD_POLL_RECEIVE_EXPEDITED :: 0x0002
AFD_POLL_SEND :: 0x0004
AFD_POLL_DISCONNECT :: 0x0008
AFD_POLL_ABORT :: 0x0010
AFD_POLL_LOCAL_CLOSE :: 0x0020
AFD_POLL_ACCEPT :: 0x0080
AFD_POLL_CONNECT_FAIL :: 0x0100
AFD_Poll_Handle_Info :: struct {
handle: win.HANDLE,
events: win.ULONG,
status: win.NTSTATUS,
}
AFD_Poll_Info :: struct {
timeout: i64,
number_of_handles: win.ULONG,
exclusive: win.ULONG,
handles: [1]AFD_Poll_Handle_Info,
}
afd_device_name := [?]u16{'\\','D','e','v','i','c','e','\\','A','f','d','\\','E','n','d','p','o','i','n','t'}
@(private="package")
_Event_Loop :: struct {
timeouts: avl.Tree(^Operation),
@@ -88,7 +120,7 @@ _Timeout :: struct {
@(private="package")
_Poll :: struct {
wait_handle: win.HANDLE,
info: AFD_Poll_Info,
}
@(private="package")
@@ -165,15 +197,7 @@ __tick :: proc(l: ^Event_Loop, timeout: time.Duration) -> (err: General_Error) {
if pool.num_outstanding(&l.operation_pool) == 0 { return nil }
actual_timeout := win.INFINITE
if queue.len(l.completed) > 0 || mpsc_count(&l.completed_oob) > 0 {
actual_timeout = 0
} else if timeout >= 0 {
actual_timeout = win.DWORD(timeout / time.Millisecond)
}
if nt, ok := next_timeout.?; ok {
actual_timeout = min(actual_timeout, win.DWORD(nt / time.Millisecond))
}
actual_timeout := compute_timeout(l, timeout, next_timeout)
if actual_timeout > 0 {
sync.atomic_store_explicit(&l.state, .Sleeping, .Release)
@@ -181,7 +205,6 @@ __tick :: proc(l: ^Event_Loop, timeout: time.Duration) -> (err: General_Error) {
// There could be a race condition where we go sleeping at the same time as things get queued
// and a wakeup isn't done because the state is not .Sleeping yet.
// So after sleeping we first check our queues.
for {
op := (^Operation)(mpsc_dequeue(&l.queue))
if op == nil { break }
@@ -193,6 +216,10 @@ __tick :: proc(l: ^Event_Loop, timeout: time.Duration) -> (err: General_Error) {
if op == nil { break }
handle_completed(op)
}
// The drains can add timeouts, and `timeout_exec` only puts those in
// `l.timeouts` without posting anything
actual_timeout = compute_timeout(l, timeout, check_timeouts(l))
}
for {
@@ -228,7 +255,7 @@ __tick :: proc(l: ^Event_Loop, timeout: time.Duration) -> (err: General_Error) {
handle_completed(op)
} else {
op_l := op.l
for !mpsc_enqueue(&op.l.completed_oob, op) {
for !mpsc_enqueue(&op_l.completed_oob, op) {
warn("oob queue filled up, QUEUE_SIZE may need increasing")
_wake_up(op_l)
win.SwitchToThread()
@@ -246,8 +273,35 @@ __tick :: proc(l: ^Event_Loop, timeout: time.Duration) -> (err: General_Error) {
actual_timeout = 0
}
// A wake, or another loop routing a completion to us, can leave work queued.
// Handle it here instead of waiting for the caller to tick again.
for {
op := (^Operation)(mpsc_dequeue(&l.queue))
if op == nil { break }
_exec(op)
}
for {
op := (^Operation)(mpsc_dequeue(&l.completed_oob))
if op == nil { break }
handle_completed(op)
}
return nil
compute_timeout :: proc(l: ^Event_Loop, timeout: time.Duration, next_timeout: Maybe(time.Duration)) -> win.DWORD {
actual: win.DWORD = win.INFINITE
if queue.len(l.completed) > 0 || mpsc_count(&l.completed_oob) > 0 {
actual = 0
} else if timeout >= 0 {
actual = win.DWORD(timeout / time.Millisecond)
}
if nt, ok := next_timeout.?; ok {
actual = min(actual, win.DWORD(nt / time.Millisecond))
}
return actual
}
check_timeouts :: proc(l: ^Event_Loop) -> (expires: Maybe(time.Duration)) {
curr := l.now
@@ -677,19 +731,6 @@ _remove :: proc(target: ^Operation) {
target._impl.timeout = (^Operation)(REMOVED)
switch target.type {
case .Poll:
win.UnregisterWaitEx(target.poll._impl.wait_handle, nil)
target.poll._impl.wait_handle = nil
ok := win.PostQueuedCompletionStatus(
g.iocp,
0,
0,
&target._impl.over,
)
ensure(ok == true, "unexpected PostQueuedCompletionStatus error")
return
case .Timeout:
if avl.remove_value(&target.l.timeouts, target) {
debug("removed timeout directly")
@@ -705,6 +746,17 @@ _remove :: proc(target: ^Operation) {
// Synchronous ops, picked up in handler.
return
case .Poll:
// The poll may have completed already, with its completion queued but not yet
// handled, `NOT_FOUND` is expected rather than exceptional.
if !win.CancelIoEx(g.afd, &target._impl.over) {
#partial switch win.System_Error(win.GetLastError()) {
case .NOT_FOUND:
// nop
case: assert(false, "unexpected CancelIoEx error")
}
}
case .Accept, .Dial, .Read, .Recv, .Send, .Write, .Send_File:
if is_pending(target._impl.over) {
handle := operation_handle(target)
@@ -802,6 +854,7 @@ g: struct{
mu: sync.Mutex,
refs: int,
iocp: win.HANDLE,
afd: win.HANDLE,
err: General_Error,
}
@@ -817,6 +870,36 @@ g_ref :: proc() -> General_Error {
if g.iocp == nil {
g.err = General_Error(win.GetLastError())
}
if g.err != nil { return g.err }
// A handle on the socket driver, used to poll sockets for readiness.
iosb: win.IO_STATUS_BLOCK
status := win.NtCreateFile(
&g.afd,
win.SYNCHRONIZE,
&{
Length = size_of(win.OBJECT_ATTRIBUTES),
ObjectName = &{
Length = u16(len(afd_device_name)*2),
MaximumLength = u16(len(afd_device_name)*2),
Buffer = raw_data(afd_device_name[:]),
},
},
&iosb,
nil,
0,
win.FILE_SHARE_READ|win.FILE_SHARE_WRITE,
win.FILE_OPEN,
0,
nil,
0,
)
if syserr := win.System_Error(win.RtlNtStatusToDosError(status)); syserr != .SUCCESS {
g.err = General_Error(syserr)
} else if win.CreateIoCompletionPort(g.afd, g.iocp, 0, 0) != g.iocp {
g.err = General_Error(win.GetLastError())
}
}
sync.atomic_add(&g.refs, 1)
@@ -828,6 +911,7 @@ g_unref :: proc() {
sync.guard(&g.mu)
if sync.atomic_sub(&g.refs, 1) == 1 {
if g.afd != nil { win.CloseHandle(g.afd) }
win.CloseHandle(g.iocp)
g.err = nil
}
@@ -850,7 +934,7 @@ operation_handle :: proc(op: ^Operation) -> win.HANDLE {
case .Recv: return win.HANDLE(uintptr(net.any_socket_to_socket(op.recv.socket)))
case .Send: return win.HANDLE(uintptr(net.any_socket_to_socket(op.send.socket)))
case .Send_File: return win.HANDLE(uintptr(net.any_socket_to_socket(op.sendfile.socket)))
case .Poll: return win.HANDLE(uintptr(net.any_socket_to_socket(op.poll.socket)))
case .Poll: return g.afd
case .Stat: return win.HANDLE(uintptr(op.stat.handle))
case .Timeout, .Open, ._Splice, ._Link_Timeout, ._Remove, .None:
@@ -925,6 +1009,9 @@ accept_exec :: proc(op: ^Operation) -> Op_Result {
return .Pending
} else if op._impl.over.Internal == nil {
op.accept.err = net._accept_error()
} else {
link_timeout(op, op.accept.expires)
return .Pending
}
}
@@ -1026,6 +1113,9 @@ dial_exec :: proc(op: ^Operation) -> (result: Op_Result) {
return .Pending
} else if op._impl.over.Internal == nil {
op.dial.err = net._dial_error()
} else {
link_timeout(op, op.dial.expires)
return .Pending
}
}
@@ -1085,6 +1175,13 @@ read_exec :: proc(op: ^Operation) -> Op_Result {
return .Pending
}
op.read.err = FS_Error(err)
} else {
// The read completed synchronously with a failure status. `FILE_SKIP_COMPLETION_PORT_ON_SUCCESS`
// only suppresses the completion packet on success, so one is still queued for
// this. Returning `.Done` here would complete the operation a second time, on an
// Operation that has already been recycled into the pool.
link_timeout(op, op.read.expires)
return .Pending
}
}
@@ -1159,6 +1256,9 @@ write_exec :: proc(op: ^Operation) -> Op_Result {
return .Pending
}
op.write.err = FS_Error(err)
} else {
link_timeout(op, op.write.expires)
return .Pending
}
}
@@ -1252,6 +1352,9 @@ recv_exec :: proc(op: ^Operation) -> Op_Result {
case TCP_Socket: op.recv.err = net._tcp_recv_error()
case UDP_Socket: op.recv.err = net._udp_recv_error()
}
} else {
link_timeout(op, op.recv.expires)
return .Pending
}
}
@@ -1370,6 +1473,9 @@ send_exec :: proc(op: ^Operation) -> Op_Result {
case TCP_Socket: op.send.err = net._tcp_send_error()
case UDP_Socket: op.send.err = net._udp_send_error()
}
} else {
link_timeout(op, op.send.expires)
return .Pending
}
}
@@ -1459,6 +1565,9 @@ sendfile_exec :: proc(op: ^Operation) -> Op_Result {
return .Pending
} else if op._impl.over.Internal == nil {
op.sendfile.err = net._tcp_send_error()
} else {
link_timeout(op, op.sendfile.expires)
return .Pending
}
}
@@ -1505,84 +1614,91 @@ sendfile_callback :: proc(op: ^Operation) -> Op_Result {
@(require_results)
poll_exec :: proc(op: ^Operation) -> Op_Result {
assert(op.type == .Poll)
op._impl.over = {} // Operations are recycled, clear stale state from a previous use.
events: i32 = win.FD_CLOSE
events: win.ULONG = AFD_POLL_ABORT|AFD_POLL_DISCONNECT|AFD_POLL_LOCAL_CLOSE|AFD_POLL_CONNECT_FAIL
switch op.poll.event {
case .Send: events |= win.FD_WRITE|win.FD_CONNECT
case .Receive: events |= win.FD_READ|win.FD_ACCEPT
case .Receive: events |= AFD_POLL_RECEIVE|AFD_POLL_RECEIVE_EXPEDITED|AFD_POLL_ACCEPT
case .Send: events |= AFD_POLL_SEND
case:
op.poll.result = .Invalid_Argument
return .Done
}
op._impl.over.hEvent = win.WSACreateEvent()
if win.WSAEventSelect(
// AFD needs the socket underneath any layered service providers.
base: win.SOCKET
bytes: win.DWORD
if win.WSAIoctl(
win.SOCKET(net.any_socket_to_socket(op.poll.socket)),
op._impl.over.hEvent,
events,
SIO_BASE_HANDLE,
nil, 0,
&base, size_of(base),
&bytes, nil, nil,
) != 0 {
#partial switch win.System_Error(win.GetLastError()) {
#partial switch win.System_Error(win.WSAGetLastError()) {
case .WSAEINVAL, .WSAENOTSOCK: op.poll.result = .Invalid_Argument
case: op.poll.result = .Error
}
return .Done
}
timeout := win.INFINITE
// A negative timeout is relative, in 100ns units.
timeout := max(i64)
if op.poll.expires != {} {
diff := max(0, time.diff(op.l.now, op.poll.expires))
timeout = win.DWORD(diff / time.Millisecond)
timeout = -i64(diff / 100)
}
ok := win.RegisterWaitForSingleObject(
&op.poll._impl.wait_handle,
op._impl.over.hEvent,
wait_callback,
op,
timeout,
win.WT_EXECUTEINWAITTHREAD|win.WT_EXECUTEONLYONCE,
op.poll._impl.info = {
timeout = timeout,
number_of_handles = 1,
handles = {{handle = win.HANDLE(uintptr(base)), events = events}},
}
// The OVERLAPPED doubles as the IO_STATUS_BLOCK, their first two fields line up.
status := win.NtDeviceIoControlFile(
g.afd,
nil,
nil,
&op._impl.over,
win.PIO_STATUS_BLOCK(rawptr(&op._impl.over)),
IOCTL_AFD_POLL,
&op.poll._impl.info,
size_of(AFD_Poll_Info),
&op.poll._impl.info,
size_of(AFD_Poll_Info),
)
ensure(ok == true, "unexpected RegisterWaitForSingleObject error")
return .Pending
wait_callback :: proc "system" (lpParameter: win.PVOID, TimerOrWaitFired: win.BOOLEAN) {
op := (^Operation)(lpParameter)
assert_contextless(op.type == .Poll)
if TimerOrWaitFired {
op.poll.result = .Timeout
}
ok := win.PostQueuedCompletionStatus(
g.iocp,
0,
0,
&op._impl.over,
)
ensure_contextless(ok == true, "unexpected PostQueuedCompletionStatus error")
// The AFD handle is not set to skip completion on success, so a completion is
// queued even when this finishes synchronously.
#partial switch win.System_Error(win.RtlNtStatusToDosError(status)) {
case .SUCCESS, .IO_PENDING:
return .Pending
case:
op.poll.result = .Error
return .Done
}
}
poll_callback :: proc(op: ^Operation) {
assert(op.type == .Poll)
if op._impl.over.hEvent != nil {
win.WSACloseEvent(op._impl.over.hEvent)
}
if op.poll._impl.wait_handle != nil {
win.UnregisterWaitEx(op.poll._impl.wait_handle, nil)
}
if op.poll.result != nil {
return
}
_, err := get_result(op._impl.over)
#partial switch err {
case .SUCCESS:
case:
// AFD reports a timeout by coming back with no handles.
if op.poll._impl.info.number_of_handles == 0 {
op.poll.result = .Timeout
return
}
if _, err := get_result(op._impl.over); err != .SUCCESS {
op.poll.result = .Error
return
}
if op.poll._impl.info.handles[0].events & (AFD_POLL_ABORT|AFD_POLL_CONNECT_FAIL) != 0 {
op.poll.result = .Error
}
}

View File

@@ -1626,6 +1626,9 @@ Poll a socket for readiness.
NOTE: this is provided to help with "legacy" APIs that require polling behavior.
If you can avoid it and use the other procs in this package, do so.
NOTE: on Windows only one poll per socket is delivered, a second poll on the same
socket does not complete.
Any user data can be set on the returned operation's `user_data` field.
Polymorphic variants for type safe user data are available under `poll_poly`, `poll_poly2`, and `poll_poly3`.
@@ -1656,6 +1659,9 @@ Poll a socket for readiness.
NOTE: this is provided to help with "legacy" APIs that require polling behavior.
If you can avoid it and use the other procs in this package, do so.
NOTE: on Windows only one poll per socket is delivered, a second poll on the same
socket does not complete.
This procedure uses polymorphism for type safe user data up to a certain size.
Inputs:
@@ -1690,6 +1696,9 @@ Poll a socket for readiness.
NOTE: this is provided to help with "legacy" APIs that require polling behavior.
If you can avoid it and use the other procs in this package, do so.
NOTE: on Windows only one poll per socket is delivered, a second poll on the same
socket does not complete.
This procedure uses polymorphism for type safe user data up to a certain size.
Inputs:
@@ -1725,6 +1734,9 @@ Poll a socket for readiness.
NOTE: this is provided to help with "legacy" APIs that require polling behavior.
If you can avoid it and use the other procs in this package, do so.
NOTE: on Windows only one poll per socket is delivered, a second poll on the same
socket does not complete.
This procedure uses polymorphism for type safe user data up to a certain size.
Inputs:

View File

@@ -7,6 +7,19 @@ foreign import ntdll_lib "system:ntdll.lib"
foreign ntdll_lib {
RtlGetVersion :: proc(lpVersionInformation: ^OSVERSIONINFOEXW) -> NTSTATUS ---
NtDeviceIoControlFile :: proc(
FileHandle: HANDLE,
Event: HANDLE,
ApcRoutine: PIO_APC_ROUTINE,
ApcContext: rawptr,
IoStatusBlock: PIO_STATUS_BLOCK,
IoControlCode: ULONG,
InputBuffer: rawptr,
InputBufferLength: ULONG,
OutputBuffer: rawptr,
OutputBufferLength: ULONG,
) -> NTSTATUS ---
NtQueryInformationProcess :: proc(
ProcessHandle: HANDLE,

View File

@@ -2146,9 +2146,8 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta
gb_internal bool check_single_target_feature_is_valid(String const &feature_list, String const &feature) {
String_Iterator it = {feature_list, 0};
for (;;) {
String str = string_split_iterator(&it, ',');
if (str == "") break;
String str = {};
while (string_split_iterator_next(&it, ',', &str)) {
if (str == feature) {
return true;
}
@@ -2160,8 +2159,8 @@ gb_internal bool check_single_target_feature_is_valid(String const &feature_list
gb_internal bool check_target_feature_is_valid(String const &feature, TargetArchKind arch, String *invalid) {
String feature_list = target_features_list[arch];
String_Iterator it = {feature, 0};
for (;;) {
String str = string_split_iterator(&it, ',');
String str = {};
while (string_split_iterator_next(&it, ',', &str)) {
String feature_str = str;
if (string_starts_with(feature_str, '+') || string_starts_with(feature_str, '-')) {
feature_str = substring(feature_str, 1, feature_str.len);
@@ -2169,7 +2168,6 @@ gb_internal bool check_target_feature_is_valid(String const &feature, TargetArch
return false;
}
}
if (feature_str == "") break;
if (!check_single_target_feature_is_valid(feature_list, feature_str)) {
if (invalid) *invalid = str;
return false;
@@ -2181,10 +2179,8 @@ gb_internal bool check_target_feature_is_valid(String const &feature, TargetArch
gb_internal bool check_target_feature_is_valid_globally(String const &feature, String *invalid) {
String_Iterator it = {feature, 0};
for (;;) {
String str = string_split_iterator(&it, ',');
if (str == "") break;
String str = {};
while (string_split_iterator_next(&it, ',', &str)) {
bool valid = false;
for (int arch = TargetArch_Invalid; arch < TargetArch_COUNT; arch += 1) {
if (check_target_feature_is_valid(str, cast(TargetArchKind)arch, invalid)) {
@@ -2208,15 +2204,19 @@ gb_internal bool check_target_feature_is_valid_for_target_arch(String const &fea
gb_internal bool check_target_feature_is_enabled(String const &feature, String *not_enabled) {
String_Iterator it = {feature, 0};
for (;;) {
String str = string_split_iterator(&it, ',');
String str = {};
while (string_split_iterator_next(&it, ',', &str)) {
String feature_str = str;
bool want_enabled = true;
if (string_starts_with(feature_str, '+') || string_starts_with(feature_str, '-')) {
want_enabled = feature_str[0] == '+';
feature_str = substring(feature_str, 1, feature_str.len);
}
if (feature_str == "") break;
if (feature_str == "") {
// a bare sign names no feature, which cannot be enabled
if (not_enabled) *not_enabled = str;
return false;
}
String plus_str = concatenate_strings(temporary_allocator(), make_string_c("+"), feature_str);
String minus_str = concatenate_strings(temporary_allocator(), make_string_c("-"), feature_str);
@@ -2240,9 +2240,8 @@ gb_internal bool check_target_feature_is_enabled(String const &feature, String *
gb_internal bool check_target_feature_is_superset_of(String const &superset, String const &of, String *missing) {
String_Iterator it = {of, 0};
for (;;) {
String str = string_split_iterator(&it, ',');
if (str == "") break;
String str = {};
while (string_split_iterator_next(&it, ',', &str)) {
if (!check_single_target_feature_is_valid(superset, str)) {
if (missing) *missing = str;
return false;

View File

@@ -3164,10 +3164,9 @@ gb_internal bool lb_generate_code(lbGenerator *gen) {
gbString llvm_features = gb_string_make(temporary_allocator(), "");
String_Iterator it = {build_context.target_features_string, 0};
String str = {};
bool first = true;
for (;;) {
String str = string_split_iterator(&it, ',');
if (str == "") break;
while (string_split_iterator_next(&it, ',', &str)) {
if (!first) {
llvm_features = gb_string_appendc(llvm_features, ",");
}

View File

@@ -322,6 +322,21 @@ gb_internal IntegerDivisionByZeroKind lb_check_for_integer_division_by_zero_beha
}
// LLVM has srem(min(Integer_Type), -1) as UB and it raises an FP exception on a hardware
// divide, yet `x % -1` is 0 for every x; `x srem 1` is 0 too and cannot trap, so a runtime
// -1 divisor can be swapped for 1. Vectorizable.
gb_internal LLVMValueRef lb_srem_safe_divisor(lbProcedure *p, LLVMValueRef rhs) {
LLVMValueRef minus_one = LLVMConstAllOnes(LLVMTypeOf(rhs));
// build 1 as neg(-1), this folds for both scalars and vectors
LLVMValueRef one = LLVMBuildNeg(p->builder, minus_one, "");
if (LLVMIsAConstantInt(rhs)) {
return rhs == minus_one ? one : rhs;
}
LLVMValueRef is_minus_one = LLVMBuildICmp(p->builder, LLVMIntEQ, rhs, minus_one, "");
return LLVMBuildSelect(p->builder, is_minus_one, one, rhs, "");
}
// implements %% (the remainder/floored mod operator) on signed integers;
// this is branchless and vectorizable, so it also covers vectors
gb_internal LLVMValueRef lb_emit_signed_floor_mod(lbProcedure *p, LLVMValueRef lhs, LLVMValueRef rhs) {
@@ -329,24 +344,11 @@ gb_internal LLVMValueRef lb_emit_signed_floor_mod(lbProcedure *p, LLVMValueRef l
// and works for arbitrary precision integers, but the add can wrap at finite precision
// the Odin spec mandates min(Integer_Type) %% -1 must be 0,
// but LLVM has srem(min(Integer_Type), -1) as UB and results in FP exception;
// since x %% -1 == 0 for every x, a constant rhs = -1 can fold,
// and a runtime -1 can be swapped with 1 (x srem 1 is 0 for every x, no exceptions)
LLVMValueRef minus_one = LLVMConstAllOnes(LLVMTypeOf(rhs));
LLVMValueRef safe_rhs = rhs;
if (LLVMIsAConstantInt(rhs)) {
if (rhs == minus_one) {
return LLVMConstNull(LLVMTypeOf(rhs)); // the entire %% op folds to 0
}
} else {
// safe_rhs = (rhs == -1) ? 1 : rhs
// vectorizable construction,
// build 1 as neg(-1), this folds for both scalars and vectors
LLVMValueRef one = LLVMBuildNeg(p->builder, minus_one, "");
LLVMValueRef is_minus_one = LLVMBuildICmp(p->builder, LLVMIntEQ, rhs, minus_one, "");
safe_rhs = LLVMBuildSelect(p->builder, is_minus_one, one, rhs, "");
// a constant rhs = -1 can fold the whole operation, a runtime one is handled by the swap
if (LLVMIsAConstantInt(rhs) && rhs == LLVMConstAllOnes(LLVMTypeOf(rhs))) {
return LLVMConstNull(LLVMTypeOf(rhs)); // the entire %% op folds to 0
}
LLVMValueRef r = LLVMBuildSRem(p->builder, lhs, safe_rhs, "");
LLVMValueRef r = LLVMBuildSRem(p->builder, lhs, lb_srem_safe_divisor(p, rhs), "");
// srem truncs to 0, so r needs a +rhs correction when the operands signs differ (and r != 0)
// so we implement
// r = lhs % rhs
@@ -498,9 +500,10 @@ gb_internal bool lb_try_direct_vector_arith(lbProcedure *p, TokenKind op, lbValu
}
break;
case Token_Mod:
{
auto *call = is_type_unsigned(integral_type) ? LLVMBuildURem : LLVMBuildSRem;
z = call(p->builder, x, y, "");
if (is_type_unsigned(integral_type)) {
z = LLVMBuildURem(p->builder, x, y, "");
} else {
z = LLVMBuildSRem(p->builder, x, lb_srem_safe_divisor(p, y), "");
}
break;
case Token_ModMod:
@@ -610,9 +613,10 @@ gb_internal bool lb_try_direct_vector_arith(lbProcedure *p, TokenKind op, lbValu
}
break;
case Token_Mod:
{
auto *call = is_type_unsigned(integral_type) ? LLVMBuildURem : LLVMBuildSRem;
z = call(p->builder, x, y, "");
if (is_type_unsigned(integral_type)) {
z = LLVMBuildURem(p->builder, x, y, "");
} else {
z = LLVMBuildSRem(p->builder, x, lb_srem_safe_divisor(p, y), "");
}
break;
case Token_ModMod:
@@ -1684,7 +1688,8 @@ gb_internal LLVMValueRef lb_integer_modulo(lbProcedure *p, LLVMValueRef lhs, LLV
if (is_unsigned) {
return LLVMBuildURem(p->builder, lhs, rhs, "");
} else {
return LLVMBuildSRem(p->builder, lhs, rhs, "");
// min(Integer_Type) % -1 is 0, matching the constant folder, and must not trap
return LLVMBuildSRem(p->builder, lhs, lb_srem_safe_divisor(p, rhs), "");
}
}
};
@@ -2407,6 +2412,13 @@ gb_internal lbValue lb_emit_conv(lbProcedure *p, lbValue value, Type *t) {
// boolean -> boolean/integer
if (is_type_boolean(src) && (is_type_boolean(dst) || is_type_integer(dst))) {
LLVMValueRef b = LLVMBuildICmp(p->builder, LLVMIntNE, value.value, LLVMConstNull(lb_type(m, value.type)), "");
if (type_size_of(default_type(dst)) > 1 && is_type_different_to_arch_endianness(dst)) {
Type *platform_dst_type = integer_endian_type_to_platform_type(dst);
lbValue res = {};
res.value = LLVMBuildIntCast2(p->builder, b, lb_type(m, platform_dst_type), false, "");
res.type = t;
return lb_emit_byte_swap(p, res, t);
}
lbValue res = {};
res.value = LLVMBuildIntCast2(p->builder, b, lb_type(m, t), false, "");
res.type = t;

View File

@@ -226,10 +226,9 @@ gb_internal lbProcedure *lb_create_procedure(lbModule *m, Entity *entity, bool i
gbString feature_str = gb_string_make(temporary_allocator(), "");
String_Iterator it = {pt->Proc.enable_target_feature, 0};
String str = {};
bool first = true;
for (;;) {
String str = string_split_iterator(&it, ',');
if (str == "") break;
while (string_split_iterator_next(&it, ',', &str)) {
bool add_prefix = !(string_starts_with(str, '+') || string_starts_with(str, '-'));
if (!first) {
feature_str = gb_string_appendc(feature_str, ",");

View File

@@ -1455,12 +1455,8 @@ gb_internal bool parse_build_flags(Array<String> args) {
GB_ASSERT(value.kind == ExactValue_String);
String val = value.value_string;
String_Iterator it = {val, 0};
for (;;) {
String pkg = string_split_iterator(&it, ',');
if (pkg.len == 0) {
break;
}
String pkg = {};
while (string_split_iterator_next(&it, ',', &pkg)) {
pkg = string_trim_whitespace(pkg);
if (!string_is_valid_identifier(pkg)) {
gb_printf_err("-%.*s '%.*s' must be a valid identifier\n", LIT(name), LIT(pkg));
@@ -1478,12 +1474,8 @@ gb_internal bool parse_build_flags(Array<String> args) {
GB_ASSERT(value.kind == ExactValue_String);
String val = value.value_string;
String_Iterator it = {val, 0};
for (;;) {
String attr = string_split_iterator(&it, ',');
if (attr.len == 0) {
break;
}
String attr = {};
while (string_split_iterator_next(&it, ',', &attr)) {
attr = string_trim_whitespace(attr);
if (!string_is_valid_identifier(attr)) {
gb_printf_err("-%.*s '%.*s' must be a valid identifier\n", LIT(name), LIT(attr));
@@ -4166,9 +4158,8 @@ int main(int arg_count, char const **arg_ptr) {
} else {
String march_list = target_microarch_list[build_context.metrics.arch];
String_Iterator it = {march_list, 0};
for (;;) {
String str = string_split_iterator(&it, ',');
if (str == "") break;
String str = {};
while (string_split_iterator_next(&it, ',', &str)) {
if (str == build_context.microarch) {
// Found matching microarch
print_microarch_list = false;
@@ -4193,9 +4184,8 @@ int main(int arg_count, char const **arg_ptr) {
String march_list = target_microarch_list[build_context.metrics.arch];
String_Iterator it = {march_list, 0};
for (;;) {
String str = string_split_iterator(&it, ',');
if (str == "") break;
String str = {};
while (string_split_iterator_next(&it, ',', &str)) {
if (str == default_march) {
gb_printf("\t%.*s (default)\n", LIT(str));
} else {
@@ -4209,9 +4199,8 @@ int main(int arg_count, char const **arg_ptr) {
String default_features = get_default_features();
{
String_Iterator it = {default_features, 0};
for (;;) {
String str = string_split_iterator(&it, ',');
if (str == "") break;
String str = {};
while (string_split_iterator_next(&it, ',', &str)) {
string_set_add(&build_context.target_features_set, str);
}
}
@@ -4231,10 +4220,8 @@ int main(int arg_count, char const **arg_ptr) {
if (build_context.target_features_string.len != 0) {
String_Iterator target_it = {build_context.target_features_string, 0};
for (;;) {
String item = string_split_iterator(&target_it, ',');
if (item == "") break;
String item = {};
while (string_split_iterator_next(&target_it, ',', &item)) {
String stripped_item = item;
if (*stripped_item.text == '+' || *stripped_item.text == '-') {
stripped_item.text++;
@@ -4251,9 +4238,8 @@ int main(int arg_count, char const **arg_ptr) {
String feature_list = target_features_list[build_context.metrics.arch];
String_Iterator it = {feature_list, 0};
for (;;) {
String str = string_split_iterator(&it, ',');
if (str == "") break;
String str = {};
while (string_split_iterator_next(&it, ',', &str)) {
if (check_single_target_feature_is_valid(default_features, str)) {
if (has_ansi_terminal_colours()) {
gb_printf("\t%.*s\x1b[38;5;244m (implied by target microarch %.*s)\x1b[0m\n", LIT(str), LIT(march));

View File

@@ -293,6 +293,20 @@ gb_internal String string_split_iterator(String_Iterator *it, const char sep) {
return substring(it->str, start, end);
}
// NOTE: `string_split_iterator` returns a zero-length `String` both for an empty element and at
// exhaustion, so a loop that stops on an empty result stops at the first empty element instead.
// This skips empty elements and stops only once the iterator is exhausted.
gb_internal bool string_split_iterator_next(String_Iterator *it, char const sep, String *str_) {
while (it->pos < it->str.len) {
String str = string_split_iterator(it, sep);
if (str.len != 0) {
*str_ = str;
return true;
}
}
return false;
}
gb_internal gb_inline bool is_separator(u8 const &ch) {
return (ch == '/' || ch == '\\');
}

View File

@@ -3640,9 +3640,8 @@ gb_internal int matched_target_features(TypeProc *t) {
int matches = 0;
String_Iterator it = {t->require_target_feature, 0};
for (;;) {
String str = string_split_iterator(&it, ',');
if (str == "") break;
String str = {};
while (string_split_iterator_next(&it, ',', &str)) {
if (check_target_feature_is_valid_for_target_arch(str, nullptr)) {
matches += 1;
}

View File

@@ -244,8 +244,11 @@ wake_up :: proc(t: ^testing.T) {
}, context)
defer thread.destroy(thr)
// Should block forever until the thread calling wake_up will make it return.
ev(t, nbio.tick(), nil)
// A tick can return without progress; loop until the wake is observed.
// A lost wake would block here forever and trip the fail timeout.
for !hit {
ev(t, nbio.tick(), nil)
}
e(t, hit)
nbio.remove(accept)

View File

@@ -245,7 +245,7 @@ And it tests big send/recv buffers being handled properly.
@(test)
poll :: proc(t: ^testing.T) {
if event_loop_guard(t) {
// testing.set_fail_timeout(t, time.Minute)
testing.set_fail_timeout(t, time.Minute)
can_recv: bool
@@ -302,13 +302,25 @@ poll :: proc(t: ^testing.T) {
on_poll1 :: proc(op: ^nbio.Operation, t: ^testing.T, can_recv: ^bool) {
ev(t, op.poll.result, nil)
// Send 4 GB of data, which in my experience causes a Would_Block error because we filled up the internal buffer.
// Fill the socket until sending actually blocks. How much that takes depends
// on the machine's socket buffers, so keep sending rather than assuming a
// fixed amount does it. Nothing is reading yet, so this terminates.
buf, mem_err := make([]byte, mem.Gigabyte*4, context.temp_allocator)
ev(t, mem_err, nil)
// Use `core:net` as example external code that doesn't care about the event loop.
net.set_blocking(op.poll.socket, false)
n, send_err := net.send(op.poll.socket, buf)
n: int
send_err: net.Network_Error
for _ in 0..<16 {
sent: int
sent, send_err = net.send(op.poll.socket, buf)
n += sent
if send_err != nil {
break
}
}
ev(t, send_err, net.TCP_Send_Error.Would_Block)
log.debugf("blocking after %M", n)

View File

@@ -58,7 +58,12 @@ immediate_remove_of_sendfile :: proc(t: ^testing.T) {
}
on_recv :: proc(op: ^nbio.Operation, t: ^testing.T) {
ev(t, op.recv.err, nil)
// The server cancelled a sendfile that had already put bytes on the wire and
// then closed, which ends the connection with a reset rather than gracefully
// often enough that both have to be accepted here.
if op.recv.err != nil {
ev(t, op.recv.err, net.TCP_Recv_Error.Connection_Closed)
}
nbio.close(op.recv.socket.(net.TCP_Socket))
}
@@ -126,7 +131,12 @@ immediate_remove_of_sendfile_without_stat :: proc(t: ^testing.T) {
}
on_recv :: proc(op: ^nbio.Operation, t: ^testing.T) {
ev(t, op.recv.err, nil)
// The server cancelled a sendfile that had already put bytes on the wire and
// then closed, which ends the connection with a reset rather than gracefully
// often enough that both have to be accepted here.
if op.recv.err != nil {
ev(t, op.recv.err, net.TCP_Recv_Error.Connection_Closed)
}
nbio.close(op.recv.socket.(net.TCP_Socket))
}
@@ -212,13 +222,18 @@ remove_multiple_poll :: proc(t: ^testing.T) {
if event_loop_guard(t) {
testing.set_fail_timeout(t, time.Minute)
sock, ep := open_next_available_local_port(t)
defer nbio.close(sock)
// Two sockets rather than two polls on one socket: only one poll per socket is
// delivered on Windows, and what this tests is removal, not that.
removed_sock, removed_ep := open_next_available_local_port(t)
defer nbio.close(removed_sock)
kept_sock, kept_ep := open_next_available_local_port(t)
defer nbio.close(kept_sock)
hit: bool
first := nbio.poll(sock, .Receive, on_poll)
nbio.poll_poly2(sock, .Receive, t, &hit, on_poll2)
first := nbio.poll(removed_sock, .Receive, on_poll)
nbio.poll_poly2(kept_sock, .Receive, t, &hit, on_poll2)
on_poll :: proc(op: ^nbio.Operation) {
log.error("shouldn't be called")
@@ -235,7 +250,9 @@ remove_multiple_poll :: proc(t: ^testing.T) {
ev(t, nbio.tick(0), nil)
nbio.dial_poly(ep, t, on_dial)
// Make both readable, the removed poll must still not fire.
nbio.dial_poly(removed_ep, t, on_dial)
nbio.dial_poly(kept_ep, t, on_dial)
on_dial :: proc(op: ^nbio.Operation, t: ^testing.T) {
ev(t, op.dial.err, nil)

View File

@@ -0,0 +1,105 @@
package test_internal
import "core:testing"
// % operator (truncated remainder)
// remainder = x - y * trunc(x / y)
@(private="file")
trunc_mod :: proc(x, y: $T) -> T {
return x - y*(x/y)
}
// this seems to prevent folding at least at -o:minimal
@(private="file")
not_const :: #force_no_inline proc(v: $T) -> T { return v }
@(test)
mod_i8_exhaustive :: proc(t: ^testing.T) {
for i in -128..=127 {
for j in -128..=127 {
if j == 0 { continue }
// min(T) % -1 == 0 is tested in mod_exception,
// the trunc_mod reference itself would trap here
if i == -128 && j == -1 { continue }
x, y := i8(i), i8(j)
got := x % y
want := trunc_mod(x, y)
testing.expectf(t, got == want, "%v %% %v == %v, want %v", x, y, got, want)
}
}
}
@(test)
mod_exception :: proc(t: ^testing.T) {
// min(T) % -1 is 0, which is what the constant folder answers
#assert(min(i8) % i8(-1) == 0)
#assert(min(i16) % i16(-1) == 0)
#assert(min(i32) % i32(-1) == 0)
#assert(min(i64) % i64(-1) == 0)
#assert(min(i128) % i128(-1) == 0)
check :: proc(t: ^testing.T, $T: typeid, loc := #caller_location) {
x, y := not_const(min(T)), not_const(T(-1))
testing.expectf(t, x % y == 0, "min(%v) %% -1 (rt divisor) == %v, want 0", typeid_of(T), x % y, loc = loc)
testing.expectf(t, x % -1 == 0, "min(%v) %% -1 (const divisor) == %v, want 0", typeid_of(T), x % -1, loc = loc)
}
check(t, i8)
check(t, i16)
check(t, i32)
check(t, i64)
check(t, i128)
}
@(test)
mod_exception_vec :: proc(t: ^testing.T) {
{
// [4]i32 emits `srem <4 x i32>`
x := not_const([4]i32{min(i32), 0, -7, 5})
y := not_const([4]i32{-1, -1, -1, -1})
testing.expect_value(t, x % y, [4]i32{0, 0, 0, 0})
}
{
// [16]i32 emits scalar `srem i32`, which is the other call site
x, y: [16]i32
for i in 0..<16 {
x[i] = i == 0 ? min(i32) : i32(i) - 8
y[i] = -1
}
testing.expect_value(t, not_const(x) % not_const(y), [16]i32{})
}
}
@(test)
mod_assign :: proc(t: ^testing.T) {
// %= must agree with %
{
x := not_const(min(i32))
y := not_const(i32(-1))
x %= y
testing.expect_value(t, x, 0)
}
{
x := not_const(i64(-17))
y := not_const(i64(5))
x %= y
testing.expect_value(t, x, -17 % 5)
}
}
@(test)
mod_unsigned_unchanged :: proc(t: ^testing.T) {
// the guard is signed-only; unsigned max is all-ones and must stay a real divisor
{
x, y := not_const(max(u32)), not_const(max(u32))
testing.expect_value(t, x % y, 0)
}
{
x, y := not_const(u32(7)), not_const(max(u32))
testing.expect_value(t, x % y, 7)
}
{
x, y := not_const(u8(200)), not_const(u8(255))
testing.expect_value(t, x % y, 200)
}
}

View File

@@ -43,12 +43,9 @@ set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused
..\..\..\odin test ..\test_issue_7008.odin %COMMON% || exit /b
..\..\..\odin check ..\test_issue_7012.odin -no-entry-point %COMMON% || exit /b
..\..\..\odin check ..\test_issue_7260.odin -no-entry-point %COMMON% || exit /b
..\..\..\odin check ..\test_issue_asm_named_register_slot.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "8" || exit /b
..\..\..\odin check ..\test_issue_ellipsis_type_call.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "10" || exit /b
..\..\..\odin check ..\test_issue_foreign_redeclaration.odin -no-entry-point %COMMON% || exit /b
..\..\..\odin check ..\test_issue_foreign_redeclaration_mismatch.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "1" || exit /b
..\..\..\odin check ..\test_issue_asm_rip_register.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "6" || exit /b
..\..\..\odin check ..\test_issue_asm_template_as_value.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "10" || exit /b
..\..\..\odin build ..\test_issue_7037.odin %COMMON% -o:none || exit /b
..\..\..\odin build ..\test_issue_7188.odin %COMMON% || exit /b
clang -c ..\test_issue_sysv_abi.c -o test_issue_sysv_abi_c.o || exit /b

View File

@@ -95,16 +95,8 @@ $ODIN test ../test_issue_7356.odin $COMMON
$ODIN build ../test_issue_7167.odin $COMMON
$ODIN build ../test_issue_7188.odin $COMMON
$ODIN check ../test_issue_7260.odin -no-entry-point $COMMON_CHECK
$ODIN test ../test_issue_bool_to_be_conversion.odin $COMMON
# `asm` templates are amd64-only, so this file is empty on every other architecture
if [[ "$(uname -m)" == "x86_64" || "$(uname -m)" == "amd64" ]]; then
if [[ $($ODIN check ../test_issue_asm_named_register_slot.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 8 ]]; then
echo "SUCCESSFUL 1/1"
else
echo "SUCCESSFUL 0/1"
exit 1
fi
fi
$ODIN check ../test_issue_foreign_redeclaration.odin -no-entry-point $COMMON_CHECK
if [[ $($ODIN check ../test_issue_foreign_redeclaration_mismatch.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 1 ]]; then
echo "SUCCESSFUL 1/1"
@@ -120,22 +112,6 @@ else
exit 1
fi
# `asm` templates are amd64-only, so this file is empty on every other architecture
if [[ "$(uname -m)" == "x86_64" || "$(uname -m)" == "amd64" ]]; then
if [[ $($ODIN check ../test_issue_asm_rip_register.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 6 ]]; then
echo "SUCCESSFUL 1/1"
else
echo "SUCCESSFUL 0/1"
exit 1
fi
if [[ $($ODIN check ../test_issue_asm_template_as_value.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 10 ]]; then
echo "SUCCESSFUL 1/1"
else
echo "SUCCESSFUL 0/1"
exit 1
fi
fi
if [[ $($ODIN build ../test_issue_7108.odin $COMMON 2>&1 >/dev/null | grep -c "Error:") -eq 2 ]]; then
echo "SUCCESSFUL 1/1"
else

View File

@@ -1,41 +0,0 @@
#+build amd64
// A slot that only a named hardware register can fill (segment/control/debug/x87/MMX)
// carries no width and no register class, so it used to absorb any operand at all and
// hand the backend an instruction that does not encode.
package test_issues
// Rejected: none of these widths pair up, and only `mov`'s segment-register forms ever
// admitted them.
bad_64_32 :: asm(a: i64) -> (r: i32) { mov r, a; }
bad_8_16 :: asm(a: u8) -> (r: u16) { mov r, a; }
bad_16_8 :: asm(a: u16) -> (r: u8) { mov r, a; }
bad_64_8 :: asm(a: u64) -> (r: u8) { mov r, a; }
// Accepted: equal widths, regardless of signedness or pointer spelling.
ok_32 :: asm(a: i32) -> (r: u32) { mov r, a; }
ok_64 :: asm(a: u64) -> (r: i64) { mov r, a; }
ok_ptr :: asm(a: rawptr) -> (r: ^i32) { mov r, a; }
// Accepted: the named registers those forms are actually for.
ok_seg :: asm(a: u64) -> (r: u64) { mov %ds, a; mov r, a; }
ok_ctrl :: asm(a: u64) -> (r: u64) { mov %cr0, a; mov r, a; }
ok_dbg :: asm(a: u64) -> (r: u64) { mov %dr0, a; mov r, a; }
use :: proc() {
a8: u8
a16: u16
a32: i32
a64: i64
au64: u64
ap: rawptr
_ = bad_64_32(a64)
_ = bad_8_16(a8)
_ = bad_16_8(a16)
_ = bad_64_8(au64)
_ = ok_32(a32)
_ = ok_64(au64)
_ = ok_ptr(ap)
_ = ok_seg(au64)
_ = ok_ctrl(au64)
_ = ok_dbg(au64)
}

View File

@@ -1,15 +0,0 @@
#+build amd64
// `%rip` is in the amd64 register table, but its class carries no width, so the checker's width
// switch reached its `GB_PANIC` default arm and aborted with SIGILL instead of diagnosing. Every
// position that can name a register reached it, including `[%rip + disp]`.
package test_issues
rip_src :: asm() -> (v: u64) { mov v, %rip; }
rip_dst :: asm(x: u64) { mov %rip, x; }
rip_mem :: asm() { mov %rax, [%rip + 8]; }
rip_clob :: asm() [#clobber %rip] { nop; }
rip_in :: asm(x: u64) [x = %rip] { nop; }
rip_out :: asm() -> (r: u64) [r = %rip] { nop; }
// the nearest special-purpose register that does carry a class has to keep checking cleanly
rsp_ok :: asm() -> (v: u64) { mov v, %rsp; }

View File

@@ -1,48 +0,0 @@
#+build amd64
// A named asm template got a plain `Addressing_Value`, so every value gate let it through:
// a cast, a transmute, an `auto_cast`, a blank assignment, a polymorphic parameter and a
// comparison against `nil` all passed the checker and then aborted the compiler in the
// backend, which has no value to lower for a template. Only a direct call and a listing in
// an `asm` group are legal.
package test_issues
t :: asm(a: i32) -> (v: i32) { mov v, a; }
a32 :: asm(a: i32) -> (v: i32) { mov v, a; }
a64 :: asm(a: i64) -> (v: i64) { mov v, a; }
g :: asm { a32, a64 }
G := cast(rawptr)(t)
take_rawptr :: proc(p: rawptr) {
_ = p
}
poly :: proc(x: $T) {
_ = size_of(T)
}
bad :: proc() {
_ = cast(proc "c" (i32) -> i32)(t)
_ = transmute(proc "c" (i32) -> i32)(t)
_ = cast(rawptr)(t)
_ = transmute(uintptr)(t)
take_rawptr(auto_cast t)
take_rawptr(cast(rawptr)(t))
_ = t
poly(t)
if t == nil {
take_rawptr(nil)
}
}
// these forms must remain valid
good :: proc() -> i32 {
x := t(1)
y := (t)(2)
z := g(i32(3))
w := g(i64(4))
v := asm(a: i32) -> (v: i32) { mov v, a; }(5)
take_rawptr(G)
return x + y + z + i32(w) + v
}

View File

@@ -0,0 +1,37 @@
package test_issues
import "core:testing"
// Converting a boolean to a big-endian integer skipped the endian fixup that the integer source
// path performs, so the result carried a native bit pattern labelled big-endian. The checker
// folded the same conversion to the right value, so only the runtime disagreed.
@(test)
bool_to_big_endian :: proc(t: ^testing.T) {
b: bool = true
f: bool = false
b8v: b8 = true
b16v: b16 = true
b32v: b32 = true
b64v: b64 = true
i: int = 1
testing.expect_value(t, int(i16be(b)), 1)
testing.expect_value(t, int(u32be(b)), 1)
testing.expect_value(t, int(u64be(b)), 1)
testing.expect_value(t, int(u128be(b)), 1)
testing.expect_value(t, int(i16be(f)), 0)
testing.expect_value(t, int(i16be(b8v)), 1)
testing.expect_value(t, int(i16be(b16v)), 1)
testing.expect_value(t, int(i16be(b32v)), 1)
testing.expect_value(t, int(i16be(b64v)), 1)
// the little-endian target and the integer source were already correct
testing.expect_value(t, int(i16le(b)), 1)
testing.expect_value(t, int(i16be(i)), 1)
// the bytes have to actually be big-endian, not a native pattern relabelled
testing.expect_value(t, transmute([4]u8)u32be(b), transmute([4]u8)u32be(i))
testing.expect_value(t, transmute([4]u8)u32be(b), [4]u8{0, 0, 0, 1})
}

View File

@@ -663,7 +663,7 @@ MouseButton :: enum c.int {
MIDDLE = 2, // Mouse button middle (pressed wheel)
SIDE = 3, // Mouse button side (advanced mouse device)
EXTRA = 4, // Mouse button extra (advanced mouse device)
FORWARD = 5, // Mouse button fordward (advanced mouse device)
FORWARD = 5, // Mouse button forward (advanced mouse device)
BACK = 6, // Mouse button back (advanced mouse device)
}