Merge remote-tracking branch 'offical/master'

This commit is contained in:
2024-05-26 10:50:16 -04:00
53 changed files with 3169 additions and 3259 deletions
+32
View File
@@ -2,6 +2,38 @@ name: CI
on: [push, pull_request, workflow_dispatch]
jobs:
build_netbsd:
name: NetBSD Build, Check, and Test
runs-on: ubuntu-latest
env:
PKGSRC_BRANCH: 2024Q1
steps:
- uses: actions/checkout@v4
- name: Build, Check, and Test
timeout-minutes: 25
uses: vmactions/netbsd-vm@v1
with:
release: "10.0"
envs: PKGSRC_BRANCH
usesh: true
copyback: false
prepare: |
PKG_PATH="https://cdn.NetBSD.org/pub/pkgsrc/packages/NetBSD/$(uname -p)/$(uname -r | cut -d_ -f1)_${PKGSRC_BRANCH}/All" /usr/sbin/pkg_add pkgin
pkgin -y in gmake git bash python311
pkgin -y in libxml2 perl zstd
/usr/sbin/pkg_add https://github.com/andreas-jonsson/llvm17-netbsd-bin/releases/download/pkgsrc-current/llvm-17.0.6.tgz
/usr/sbin/pkg_add https://github.com/andreas-jonsson/llvm17-netbsd-bin/releases/download/pkgsrc-current/clang-17.0.6.tgz
ln -s /usr/pkg/bin/python3.11 /usr/bin/python3
ln -s /usr/pkg/bin/bash /bin/bash
run: |
git config --global --add safe.directory $(pwd)
gmake release
./odin version
./odin report
./odin check examples/all -vet -strict-style -target:netbsd_amd64
(cd tests/core; gmake all_bsd)
(cd tests/internal; gmake all_bsd)
(cd tests/issues; ./run.sh)
build_linux:
name: Ubuntu Build, Check, and Test
runs-on: ubuntu-latest
+3
View File
@@ -167,6 +167,9 @@ type_is_matrix :: proc($T: typeid) -> bool ---
type_has_nil :: proc($T: typeid) -> bool ---
type_is_matrix_row_major :: proc($T: typeid) -> bool where type_is_matrix(T) ---
type_is_matrix_column_major :: proc($T: typeid) -> bool where type_is_matrix(T) ---
type_is_specialization_of :: proc($T, $S: typeid) -> bool ---
type_is_variant_of :: proc($U, $V: typeid) -> bool where type_is_union(U) ---
@@ -6,6 +6,9 @@ when ODIN_DEFAULT_TO_NIL_ALLOCATOR {
} else when ODIN_DEFAULT_TO_PANIC_ALLOCATOR {
default_allocator_proc :: panic_allocator_proc
default_allocator :: panic_allocator
} else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 {
default_allocator :: default_wasm_allocator
default_allocator_proc :: wasm_allocator_proc
} else {
default_allocator :: heap_allocator
default_allocator_proc :: heap_allocator_proc
@@ -1,7 +1,7 @@
package runtime
DEFAULT_TEMP_ALLOCATOR_BACKING_SIZE: int : #config(DEFAULT_TEMP_ALLOCATOR_BACKING_SIZE, 4 * Megabyte)
NO_DEFAULT_TEMP_ALLOCATOR: bool : ODIN_OS == .Freestanding || ODIN_OS == .JS || ODIN_DEFAULT_TO_NIL_ALLOCATOR
NO_DEFAULT_TEMP_ALLOCATOR: bool : ODIN_OS == .Freestanding || ODIN_DEFAULT_TO_NIL_ALLOCATOR
when NO_DEFAULT_TEMP_ALLOCATOR {
Default_Temp_Allocator :: struct {}
+35 -1
View File
@@ -40,6 +40,24 @@ align_forward_int :: #force_inline proc(ptr, align: int) -> int {
return p
}
is_power_of_two_uint :: #force_inline proc "contextless" (x: uint) -> bool {
if x <= 0 {
return false
}
return (x & (x-1)) == 0
}
align_forward_uint :: #force_inline proc(ptr, align: uint) -> uint {
assert(is_power_of_two_uint(align))
p := ptr
modulo := p & (align-1)
if modulo != 0 {
p += align - modulo
}
return p
}
is_power_of_two_uintptr :: #force_inline proc "contextless" (x: uintptr) -> bool {
if x <= 0 {
return false
@@ -58,6 +76,18 @@ align_forward_uintptr :: #force_inline proc(ptr, align: uintptr) -> uintptr {
return p
}
is_power_of_two :: proc {
is_power_of_two_int,
is_power_of_two_uint,
is_power_of_two_uintptr,
}
align_forward :: proc {
align_forward_int,
align_forward_uint,
align_forward_uintptr,
}
mem_zero :: proc "contextless" (data: rawptr, len: int) -> rawptr {
if data == nil {
return nil
@@ -801,6 +831,10 @@ truncsfhf2 :: proc "c" (value: f32) -> __float16 {
}
}
@(link_name="__aeabi_d2h", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE)
aeabi_d2h :: proc "c" (value: f64) -> __float16 {
return truncsfhf2(f32(value))
}
@(link_name="__truncdfhf2", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE)
truncdfhf2 :: proc "c" (value: f64) -> __float16 {
@@ -1055,4 +1089,4 @@ __read_bits :: proc "contextless" (dst, src: [^]byte, offset: uintptr, size: uin
dst[j>>3] &~= 1<<(j&7)
dst[j>>3] |= the_bit<<(j&7)
}
}
}
+2 -2
View File
@@ -5,7 +5,7 @@ package runtime
import "core:sys/wasm/wasi"
_stderr_write :: proc "contextless" (data: []byte) -> (int, _OS_Errno) {
data := (wasi.ciovec_t)(data)
n, err := wasi.fd_write(1, {data})
data_iovec := (wasi.ciovec_t)(data)
n, err := wasi.fd_write(1, {data_iovec})
return int(n), _OS_Errno(err)
}
+870
View File
@@ -0,0 +1,870 @@
//+build wasm32, wasm64p32
package runtime
import "base:intrinsics"
/*
Port of emmalloc, modified for use in Odin.
Invariants:
- Per-allocation header overhead is 8 bytes, smallest allocated payload
amount is 8 bytes, and a multiple of 4 bytes.
- Acquired memory blocks are subdivided into disjoint regions that lie
next to each other.
- A region is either in used or free.
Used regions may be adjacent, and a used and unused region
may be adjacent, but not two unused ones - they would be
merged.
- Memory allocation takes constant time, unless the alloc needs to wasm_memory_grow()
or memory is very close to being exhausted.
- Free and used regions are managed inside "root regions", which are slabs
of memory acquired via wasm_memory_grow().
- Memory retrieved using wasm_memory_grow() can not be given back to the OS.
Therefore, frees are internal to the allocator.
Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
WASM_Allocator :: struct #no_copy {
// The minimum alignment of allocations.
alignment: uint,
// A region that contains as payload a single forward linked list of pointers to
// root regions of each disjoint region blocks.
list_of_all_regions: ^Root_Region,
// For each of the buckets, maintain a linked list head node. The head node for each
// free region is a sentinel node that does not actually represent any free space, but
// the sentinel is used to avoid awkward testing against (if node == freeRegionHeadNode)
// when adding and removing elements from the linked list, i.e. we are guaranteed that
// the sentinel node is always fixed and there, and the actual free region list elements
// start at free_region_buckets[i].next each.
free_region_buckets: [NUM_FREE_BUCKETS]Region,
// A bitmask that tracks the population status for each of the 64 distinct memory regions:
// a zero at bit position i means that the free list bucket i is empty. This bitmask is
// used to avoid redundant scanning of the 64 different free region buckets: instead by
// looking at the bitmask we can find in constant time an index to a free region bucket
// that contains free memory of desired size.
free_region_buckets_used: BUCKET_BITMASK_T,
// Because wasm memory can only be allocated in pages of 64k at a time, we keep any
// spilled/unused bytes that are left from the allocated pages here, first using this
// when bytes are needed.
spill: []byte,
// Mutex for thread safety, only used if the target feature "atomics" is enabled.
mu: Mutex_State,
}
// Not required to be called, called on first allocation otherwise.
wasm_allocator_init :: proc(a: ^WASM_Allocator, alignment: uint = 8) {
assert(is_power_of_two(alignment), "alignment must be a power of two")
assert(alignment > 4, "alignment must be more than 4")
a.alignment = alignment
for i in 0..<NUM_FREE_BUCKETS {
a.free_region_buckets[i].next = &a.free_region_buckets[i]
a.free_region_buckets[i].prev = a.free_region_buckets[i].next
}
if !claim_more_memory(a, 3*size_of(Region)) {
panic("wasm_allocator: initial memory could not be allocated")
}
}
global_default_wasm_allocator_data: WASM_Allocator
default_wasm_allocator :: proc() -> Allocator {
return wasm_allocator(&global_default_wasm_allocator_data)
}
wasm_allocator :: proc(a: ^WASM_Allocator) -> Allocator {
return {
data = a,
procedure = wasm_allocator_proc,
}
}
wasm_allocator_proc :: proc(a: rawptr, mode: Allocator_Mode, size, alignment: int, old_memory: rawptr, old_size: int, loc := #caller_location) -> ([]byte, Allocator_Error) {
a := (^WASM_Allocator)(a)
if a == nil {
a = &global_default_wasm_allocator_data
}
if a.alignment == 0 {
wasm_allocator_init(a)
}
switch mode {
case .Alloc:
ptr := aligned_alloc(a, uint(alignment), uint(size), loc)
if ptr == nil {
return nil, .Out_Of_Memory
}
intrinsics.mem_zero(ptr, size)
return ([^]byte)(ptr)[:size], nil
case .Alloc_Non_Zeroed:
ptr := aligned_alloc(a, uint(alignment), uint(size), loc)
if ptr == nil {
return nil, .Out_Of_Memory
}
return ([^]byte)(ptr)[:size], nil
case .Resize:
ptr := aligned_realloc(a, old_memory, uint(alignment), uint(size), loc)
if ptr == nil {
return nil, .Out_Of_Memory
}
bytes := ([^]byte)(ptr)[:size]
if size > old_size {
new_region := raw_data(bytes[old_size:])
intrinsics.mem_zero(new_region, size - old_size)
}
return bytes, nil
case .Resize_Non_Zeroed:
ptr := aligned_realloc(a, old_memory, uint(alignment), uint(size), loc)
if ptr == nil {
return nil, .Out_Of_Memory
}
return ([^]byte)(ptr)[:size], nil
case .Free:
free(a, old_memory, loc)
return nil, nil
case .Free_All, .Query_Info:
return nil, .Mode_Not_Implemented
case .Query_Features:
set := (^Allocator_Mode_Set)(old_memory)
if set != nil {
set^ = {.Alloc, .Alloc_Non_Zeroed, .Free, .Resize, .Resize_Non_Zeroed, .Query_Features }
}
return nil, nil
}
unreachable()
}
// Returns the allocated size of the allocator (both free and used).
// If `nil` is given, the global allocator is used.
wasm_allocator_size :: proc(a: ^WASM_Allocator = nil) -> (size: uint) {
a := a
if a == nil {
a = &global_default_wasm_allocator_data
}
lock(a)
defer unlock(a)
root := a.list_of_all_regions
for root != nil {
size += uint(uintptr(root.end_ptr) - uintptr(root))
root = root.next
}
size += len(a.spill)
return
}
// Returns the amount of free memory on the allocator.
// If `nil` is given, the global allocator is used.
wasm_allocator_free_space :: proc(a: ^WASM_Allocator = nil) -> (free: uint) {
a := a
if a == nil {
a = &global_default_wasm_allocator_data
}
lock(a)
defer unlock(a)
bucket_index: u64 = 0
bucket_mask := a.free_region_buckets_used
for bucket_mask != 0 {
index_add := intrinsics.count_trailing_zeros(bucket_mask)
bucket_index += index_add
bucket_mask >>= index_add
for free_region := a.free_region_buckets[bucket_index].next; free_region != &a.free_region_buckets[bucket_index]; free_region = free_region.next {
free += free_region.size - REGION_HEADER_SIZE
}
bucket_index += 1
bucket_mask >>= 1
}
free += len(a.spill)
return
}
@(private="file")
NUM_FREE_BUCKETS :: 64
@(private="file")
BUCKET_BITMASK_T :: u64
// Dynamic memory is subdivided into regions, in the format
// <size:u32> ..... <size:u32> | <size:u32> ..... <size:u32> | <size:u32> ..... <size:u32> | .....
// That is, at the bottom and top end of each memory region, the size of that region is stored. That allows traversing the
// memory regions backwards and forwards. Because each allocation must be at least a multiple of 4 bytes, the lowest two bits of
// each size field is unused. Free regions are distinguished by used regions by having the FREE_REGION_FLAG bit present
// in the size field. I.e. for free regions, the size field is odd, and for used regions, the size field reads even.
@(private="file")
FREE_REGION_FLAG :: 0x1
// Attempts to alloc more than this many bytes would cause an overflow when calculating the size of a region,
// therefore allocations larger than this are short-circuited immediately on entry.
@(private="file")
MAX_ALLOC_SIZE :: 0xFFFFFFC7
// A free region has the following structure:
// <size:uint> <prevptr> <nextptr> ... <size:uint>
@(private="file")
Region :: struct {
size: uint,
prev, next: ^Region,
_at_the_end_of_this_struct_size: uint,
}
// Each memory block starts with a Root_Region at the beginning.
// The Root_Region specifies the size of the region block, and forms a linked
// list of all Root_Regions in the program, starting with `list_of_all_regions`
// below.
@(private="file")
Root_Region :: struct {
size: u32,
next: ^Root_Region,
end_ptr: ^byte,
}
@(private="file")
Mutex_State :: enum u32 {
Unlocked = 0,
Locked = 1,
Waiting = 2,
}
@(private="file")
lock :: proc(a: ^WASM_Allocator) {
when intrinsics.has_target_feature("atomics") {
@(cold)
lock_slow :: proc(a: ^WASM_Allocator, curr_state: Mutex_State) {
new_state := curr_state // Make a copy of it
spin_lock: for spin in 0..<i32(100) {
state, ok := intrinsics.atomic_compare_exchange_weak_explicit(&a.mu, .Unlocked, new_state, .Acquire, .Consume)
if ok {
return
}
if state == .Waiting {
break spin_lock
}
for i := min(spin+1, 32); i > 0; i -= 1 {
intrinsics.cpu_relax()
}
}
// Set just in case 100 iterations did not do it
new_state = .Waiting
for {
if intrinsics.atomic_exchange_explicit(&a.mu, .Waiting, .Acquire) == .Unlocked {
return
}
assert(intrinsics.wasm_memory_atomic_wait32((^u32)(&a.mu), u32(new_state), -1) != 0)
intrinsics.cpu_relax()
}
}
if v := intrinsics.atomic_exchange_explicit(&a.mu, .Locked, .Acquire); v != .Unlocked {
lock_slow(a, v)
}
}
}
@(private="file")
unlock :: proc(a: ^WASM_Allocator) {
when intrinsics.has_target_feature("atomics") {
@(cold)
unlock_slow :: proc(a: ^WASM_Allocator) {
for {
s := intrinsics.wasm_memory_atomic_notify32((^u32)(&a.mu), 1)
if s >= 1 {
return
}
}
}
switch intrinsics.atomic_exchange_explicit(&a.mu, .Unlocked, .Release) {
case .Unlocked:
unreachable()
case .Locked:
// Okay
case .Waiting:
unlock_slow(a)
}
}
}
@(private="file")
assert_locked :: proc(a: ^WASM_Allocator) {
when intrinsics.has_target_feature("atomics") {
assert(intrinsics.atomic_load(&a.mu) != .Unlocked)
}
}
@(private="file")
has_alignment_uintptr :: proc(ptr: uintptr, #any_int alignment: uintptr) -> bool {
return ptr & (alignment-1) == 0
}
@(private="file")
has_alignment_uint :: proc(ptr: uint, alignment: uint) -> bool {
return ptr & (alignment-1) == 0
}
@(private="file")
has_alignment :: proc {
has_alignment_uintptr,
has_alignment_uint,
}
@(private="file")
REGION_HEADER_SIZE :: 2*size_of(uint)
@(private="file")
SMALLEST_ALLOCATION_SIZE :: 2*size_of(rawptr)
// Subdivide regions of free space into distinct circular doubly linked lists, where each linked list
// represents a range of free space blocks. The following function compute_free_list_bucket() converts
// an allocation size to the bucket index that should be looked at.
#assert(NUM_FREE_BUCKETS == 64, "Following function is tailored specifically for the NUM_FREE_BUCKETS == 64 case")
@(private="file")
compute_free_list_bucket :: proc(size: uint) -> uint {
if size < 128 { return (size >> 3) - 1 }
clz := intrinsics.count_leading_zeros(i32(size))
bucket_index: i32 = ((clz > 19) \
? 110 - (clz<<2) + ((i32)(size >> (u32)(29-clz)) ~ 4) \
: min( 71 - (clz<<1) + ((i32)(size >> (u32)(30-clz)) ~ 2), NUM_FREE_BUCKETS-1))
assert(bucket_index >= 0)
assert(bucket_index < NUM_FREE_BUCKETS)
return uint(bucket_index)
}
@(private="file")
prev_region :: proc(region: ^Region) -> ^Region {
prev_region_size := ([^]uint)(region)[-1]
prev_region_size = prev_region_size & ~uint(FREE_REGION_FLAG)
return (^Region)(uintptr(region)-uintptr(prev_region_size))
}
@(private="file")
next_region :: proc(region: ^Region) -> ^Region {
return (^Region)(uintptr(region)+uintptr(region.size))
}
@(private="file")
region_ceiling_size :: proc(region: ^Region) -> uint {
return ([^]uint)(uintptr(region)+uintptr(region.size))[-1]
}
@(private="file")
region_is_free :: proc(r: ^Region) -> bool {
return region_ceiling_size(r) & FREE_REGION_FLAG >= 1
}
@(private="file")
region_is_in_use :: proc(r: ^Region) -> bool {
return r.size == region_ceiling_size(r)
}
@(private="file")
region_payload_start_ptr :: proc(r: ^Region) -> [^]byte {
return ([^]byte)(r)[size_of(uint):]
}
@(private="file")
region_payload_end_ptr :: proc(r: ^Region) -> [^]byte {
return ([^]byte)(r)[r.size-size_of(uint):]
}
@(private="file")
create_used_region :: proc(ptr: rawptr, size: uint) {
assert(has_alignment(uintptr(ptr), size_of(uint)))
assert(has_alignment(size, size_of(uint)))
assert(size >= size_of(Region))
uptr := ([^]uint)(ptr)
uptr[0] = size
uptr[size/size_of(uint)-1] = size
}
@(private="file")
create_free_region :: proc(ptr: rawptr, size: uint) {
assert(has_alignment(uintptr(ptr), size_of(uint)))
assert(has_alignment(size, size_of(uint)))
assert(size >= size_of(Region))
free_region := (^Region)(ptr)
free_region.size = size
([^]uint)(ptr)[size/size_of(uint)-1] = size | FREE_REGION_FLAG
}
@(private="file")
prepend_to_free_list :: proc(region: ^Region, prepend_to: ^Region) {
assert(region_is_free(region))
region.next = prepend_to
region.prev = prepend_to.prev
prepend_to.prev = region
region.prev.next = region
}
@(private="file")
unlink_from_free_list :: proc(region: ^Region) {
assert(region_is_free(region))
region.prev.next = region.next
region.next.prev = region.prev
}
@(private="file")
link_to_free_list :: proc(a: ^WASM_Allocator, free_region: ^Region) {
assert(free_region.size >= size_of(Region))
bucket_index := compute_free_list_bucket(free_region.size-REGION_HEADER_SIZE)
free_list_head := &a.free_region_buckets[bucket_index]
free_region.prev = free_list_head
free_region.next = free_list_head.next
free_list_head.next = free_region
free_region.next.prev = free_region
a.free_region_buckets_used |= BUCKET_BITMASK_T(1) << bucket_index
}
@(private="file")
claim_more_memory :: proc(a: ^WASM_Allocator, num_bytes: uint) -> bool {
PAGE_SIZE :: 64 * 1024
page_alloc :: proc(page_count: int) -> []byte {
prev_page_count := intrinsics.wasm_memory_grow(0, uintptr(page_count))
if prev_page_count < 0 { return nil }
ptr := ([^]byte)(uintptr(prev_page_count) * PAGE_SIZE)
return ptr[:page_count * PAGE_SIZE]
}
alloc :: proc(a: ^WASM_Allocator, num_bytes: uint) -> (bytes: [^]byte) #no_bounds_check {
if uint(len(a.spill)) >= num_bytes {
bytes = raw_data(a.spill[:num_bytes])
a.spill = a.spill[num_bytes:]
return
}
pages := int((num_bytes / PAGE_SIZE) + 1)
allocated := page_alloc(pages)
if allocated == nil { return nil }
// If the allocated memory is a direct continuation of the spill from before,
// we can just extend the spill.
spill_end := uintptr(raw_data(a.spill)) + uintptr(len(a.spill))
if spill_end == uintptr(raw_data(allocated)) {
raw_spill := transmute(^Raw_Slice)(&a.spill)
raw_spill.len += len(allocated)
} else {
// Otherwise, we have to "waste" the previous spill.
// Now this is probably uncommon, and will only happen if another code path
// is also requesting pages.
a.spill = allocated
}
bytes = raw_data(a.spill)
a.spill = a.spill[num_bytes:]
return
}
num_bytes := num_bytes
num_bytes = align_forward(num_bytes, a.alignment)
start_ptr := alloc(a, uint(num_bytes))
if start_ptr == nil { return false }
assert(has_alignment(uintptr(start_ptr), align_of(uint)))
end_ptr := start_ptr[num_bytes:]
end_sentinel_region := (^Region)(end_ptr[-size_of(Region):])
create_used_region(end_sentinel_region, size_of(Region))
// If we are the sole user of wasm_memory_grow(), it will feed us continuous/consecutive memory addresses - take advantage
// of that if so: instead of creating two disjoint memory regions blocks, expand the previous one to a larger size.
prev_alloc_end_address := a.list_of_all_regions != nil ? a.list_of_all_regions.end_ptr : nil
if start_ptr == prev_alloc_end_address {
prev_end_sentinel := prev_region((^Region)(start_ptr))
assert(region_is_in_use(prev_end_sentinel))
prev_region := prev_region(prev_end_sentinel)
a.list_of_all_regions.end_ptr = end_ptr
// Two scenarios, either the last region of the previous block was in use, in which case we need to create
// a new free region in the newly allocated space; or it was free, in which case we can extend that region
// to cover a larger size.
if region_is_free(prev_region) {
new_free_region_size := uint(uintptr(end_sentinel_region) - uintptr(prev_region))
unlink_from_free_list(prev_region)
create_free_region(prev_region, new_free_region_size)
link_to_free_list(a, prev_region)
return true
}
start_ptr = start_ptr[-size_of(Region):]
} else {
create_used_region(start_ptr, size_of(Region))
new_region_block := (^Root_Region)(start_ptr)
new_region_block.next = a.list_of_all_regions
new_region_block.end_ptr = end_ptr
a.list_of_all_regions = new_region_block
start_ptr = start_ptr[size_of(Region):]
}
create_free_region(start_ptr, uint(uintptr(end_sentinel_region)-uintptr(start_ptr)))
link_to_free_list(a, (^Region)(start_ptr))
return true
}
@(private="file")
validate_alloc_size :: proc(size: uint) -> uint {
#assert(size_of(uint) >= size_of(uintptr))
#assert(size_of(uint) % size_of(uintptr) == 0)
// NOTE: emmalloc aligns this forward on pointer size, but I think that is a mistake and will
// do bad on wasm64p32.
validated_size := size > SMALLEST_ALLOCATION_SIZE ? align_forward(size, size_of(uint)) : SMALLEST_ALLOCATION_SIZE
assert(validated_size >= size) // Assert we haven't wrapped.
return validated_size
}
@(private="file")
allocate_memory :: proc(a: ^WASM_Allocator, alignment: uint, size: uint, loc := #caller_location) -> rawptr {
attempt_allocate :: proc(a: ^WASM_Allocator, free_region: ^Region, alignment, size: uint) -> rawptr {
assert_locked(a)
free_region := free_region
payload_start_ptr := uintptr(region_payload_start_ptr(free_region))
payload_start_ptr_aligned := align_forward(payload_start_ptr, uintptr(alignment))
payload_end_ptr := uintptr(region_payload_end_ptr(free_region))
if payload_start_ptr_aligned + uintptr(size) > payload_end_ptr {
return nil
}
// We have enough free space, so the memory allocation will be made into this region. Remove this free region
// from the list of free regions: whatever slop remains will be later added back to the free region pool.
unlink_from_free_list(free_region)
// Before we proceed further, fix up the boundary between this and the preceding region,
// so that the boundary between the two regions happens at a right spot for the payload to be aligned.
if payload_start_ptr != payload_start_ptr_aligned {
prev := prev_region(free_region)
assert(region_is_in_use(prev))
region_boundary_bump_amount := payload_start_ptr_aligned - payload_start_ptr
new_this_region_size := free_region.size - uint(region_boundary_bump_amount)
create_used_region(prev, prev.size + uint(region_boundary_bump_amount))
free_region = (^Region)(uintptr(free_region) + region_boundary_bump_amount)
free_region.size = new_this_region_size
}
// Next, we need to decide whether this region is so large that it should be split into two regions,
// one representing the newly used memory area, and at the high end a remaining leftover free area.
// This splitting to two is done always if there is enough space for the high end to fit a region.
// Carve 'size' bytes of payload off this region. So,
// [sz prev next sz]
// becomes
// [sz payload sz] [sz prev next sz]
if size_of(Region) + REGION_HEADER_SIZE + size <= free_region.size {
new_free_region := (^Region)(uintptr(free_region) + REGION_HEADER_SIZE + uintptr(size))
create_free_region(new_free_region, free_region.size - size - REGION_HEADER_SIZE)
link_to_free_list(a, new_free_region)
create_used_region(free_region, size + REGION_HEADER_SIZE)
} else {
// There is not enough space to split the free memory region into used+free parts, so consume the whole
// region as used memory, not leaving a free memory region behind.
// Initialize the free region as used by resetting the ceiling size to the same value as the size at bottom.
([^]uint)(uintptr(free_region) + uintptr(free_region.size))[-1] = free_region.size
}
return rawptr(uintptr(free_region) + size_of(uint))
}
assert_locked(a)
assert(is_power_of_two(alignment))
assert(size <= MAX_ALLOC_SIZE, "allocation too big", loc=loc)
alignment := alignment
alignment = max(alignment, a.alignment)
size := size
size = validate_alloc_size(size)
// Attempt to allocate memory starting from smallest bucket that can contain the required amount of memory.
// Under normal alignment conditions this should always be the first or second bucket we look at, but if
// performing an allocation with complex alignment, we may need to look at multiple buckets.
bucket_index := compute_free_list_bucket(size)
bucket_mask := a.free_region_buckets_used >> bucket_index
// Loop through each bucket that has free regions in it, based on bits set in free_region_buckets_used bitmap.
for bucket_mask != 0 {
index_add := intrinsics.count_trailing_zeros(bucket_mask)
bucket_index += uint(index_add)
bucket_mask >>= index_add
assert(bucket_index <= NUM_FREE_BUCKETS-1)
assert(a.free_region_buckets_used & (BUCKET_BITMASK_T(1) << bucket_index) > 0)
free_region := a.free_region_buckets[bucket_index].next
assert(free_region != nil)
if free_region != &a.free_region_buckets[bucket_index] {
ptr := attempt_allocate(a, free_region, alignment, size)
if ptr != nil {
return ptr
}
// We were not able to allocate from the first region found in this bucket, so penalize
// the region by cycling it to the end of the doubly circular linked list. (constant time)
// This provides a randomized guarantee that when performing allocations of size k to a
// bucket of [k-something, k+something] range, we will not always attempt to satisfy the
// allocation from the same available region at the front of the list, but we try each
// region in turn.
unlink_from_free_list(free_region)
prepend_to_free_list(free_region, &a.free_region_buckets[bucket_index])
// But do not stick around to attempt to look at other regions in this bucket - move
// to search the next populated bucket index if this did not fit. This gives a practical
// "allocation in constant time" guarantee, since the next higher bucket will only have
// regions that are all of strictly larger size than the requested allocation. Only if
// there is a difficult alignment requirement we may fail to perform the allocation from
// a region in the next bucket, and if so, we keep trying higher buckets until one of them
// works.
bucket_index += 1
bucket_mask >>= 1
} else {
// This bucket was not populated after all with any regions,
// but we just had a stale bit set to mark a populated bucket.
// Reset the bit to update latest status so that we do not
// redundantly look at this bucket again.
a.free_region_buckets_used &= ~(BUCKET_BITMASK_T(1) << bucket_index)
bucket_mask ~= 1
}
assert((bucket_index == NUM_FREE_BUCKETS && bucket_mask == 0) || (bucket_mask == a.free_region_buckets_used >> bucket_index))
}
// None of the buckets were able to accommodate an allocation. If this happens we are almost out of memory.
// The largest bucket might contain some suitable regions, but we only looked at one region in that bucket, so
// as a last resort, loop through more free regions in the bucket that represents the largest allocations available.
// But only if the bucket representing largest allocations available is not any of the first thirty buckets,
// these represent allocatable areas less than <1024 bytes - which could be a lot of scrap.
// In such case, prefer to claim more memory right away.
largest_bucket_index := NUM_FREE_BUCKETS - 1 - intrinsics.count_leading_zeros(a.free_region_buckets_used)
// free_region will be null if there is absolutely no memory left. (all buckets are 100% used)
free_region := a.free_region_buckets_used > 0 ? a.free_region_buckets[largest_bucket_index].next : nil
// The 30 first free region buckets cover memory blocks < 2048 bytes, so skip looking at those here (too small)
if a.free_region_buckets_used >> 30 > 0 {
// Look only at a constant number of regions in this bucket max, to avoid bad worst case behavior.
// If this many regions cannot find free space, we give up and prefer to claim more memory instead.
max_regions_to_try_before_giving_up :: 99
num_tries_left := max_regions_to_try_before_giving_up
for ; free_region != &a.free_region_buckets[largest_bucket_index] && num_tries_left > 0; num_tries_left -= 1 {
ptr := attempt_allocate(a, free_region, alignment, size)
if ptr != nil {
return ptr
}
free_region = free_region.next
}
}
// We were unable to find a free memory region. Must claim more memory!
num_bytes_to_claim := size+size_of(Region)*3
if alignment > a.alignment {
num_bytes_to_claim += alignment
}
success := claim_more_memory(a, num_bytes_to_claim)
if (success) {
// Try allocate again with the newly available memory.
return allocate_memory(a, alignment, size)
}
// also claim_more_memory failed, we are really really constrained :( As a last resort, go back to looking at the
// bucket we already looked at above, continuing where the above search left off - perhaps there are
// regions we overlooked the first time that might be able to satisfy the allocation.
if free_region != nil {
for free_region != &a.free_region_buckets[largest_bucket_index] {
ptr := attempt_allocate(a, free_region, alignment, size)
if ptr != nil {
return ptr
}
free_region = free_region.next
}
}
// Fully out of memory.
return nil
}
@(private="file")
aligned_alloc :: proc(a: ^WASM_Allocator, alignment, size: uint, loc := #caller_location) -> rawptr {
lock(a)
defer unlock(a)
return allocate_memory(a, alignment, size, loc)
}
@(private="file")
free :: proc(a: ^WASM_Allocator, ptr: rawptr, loc := #caller_location) {
if ptr == nil {
return
}
region_start_ptr := uintptr(ptr) - size_of(uint)
region := (^Region)(region_start_ptr)
assert(has_alignment(region_start_ptr, size_of(uint)))
lock(a)
defer unlock(a)
size := region.size
assert(region_is_in_use(region), "double free", loc=loc)
prev_region_size_field := ([^]uint)(region)[-1]
prev_region_size := prev_region_size_field & ~uint(FREE_REGION_FLAG)
if prev_region_size_field != prev_region_size {
prev_region := (^Region)(uintptr(region) - uintptr(prev_region_size))
unlink_from_free_list(prev_region)
region_start_ptr = uintptr(prev_region)
size += prev_region_size
}
next_reg := next_region(region)
size_at_end := (^uint)(region_payload_end_ptr(next_reg))^
if next_reg.size != size_at_end {
unlink_from_free_list(next_reg)
size += next_reg.size
}
create_free_region(rawptr(region_start_ptr), size)
link_to_free_list(a, (^Region)(region_start_ptr))
}
@(private="file")
aligned_realloc :: proc(a: ^WASM_Allocator, ptr: rawptr, alignment, size: uint, loc := #caller_location) -> rawptr {
attempt_region_resize :: proc(a: ^WASM_Allocator, region: ^Region, size: uint) -> bool {
lock(a)
defer unlock(a)
// First attempt to resize this region, if the next region that follows this one
// is a free region.
next_reg := next_region(region)
next_region_end_ptr := uintptr(next_reg) + uintptr(next_reg.size)
size_at_ceiling := ([^]uint)(next_region_end_ptr)[-1]
if next_reg.size != size_at_ceiling { // Next region is free?
assert(region_is_free(next_reg))
new_next_region_start_ptr := uintptr(region) + uintptr(size)
assert(has_alignment(new_next_region_start_ptr, size_of(uint)))
// Next region does not shrink to too small size?
if new_next_region_start_ptr + size_of(Region) <= next_region_end_ptr {
unlink_from_free_list(next_reg)
create_free_region(rawptr(new_next_region_start_ptr), uint(next_region_end_ptr - new_next_region_start_ptr))
link_to_free_list(a, (^Region)(new_next_region_start_ptr))
create_used_region(region, uint(new_next_region_start_ptr - uintptr(region)))
return true
}
// If we remove the next region altogether, allocation is satisfied?
if new_next_region_start_ptr <= next_region_end_ptr {
unlink_from_free_list(next_reg)
create_used_region(region, region.size + next_reg.size)
return true
}
} else {
// Next region is an used region - we cannot change its starting address. However if we are shrinking the
// size of this region, we can create a new free region between this and the next used region.
if size + size_of(Region) <= region.size {
free_region_size := region.size - size
create_used_region(region, size)
free_region := (^Region)(uintptr(region) + uintptr(size))
create_free_region(free_region, free_region_size)
link_to_free_list(a, free_region)
return true
} else if size <= region.size {
// Caller was asking to shrink the size, but due to not being able to fit a full Region in the shrunk
// area, we cannot actually do anything. This occurs if the shrink amount is really small. In such case,
// just call it success without doing any work.
return true
}
}
return false
}
if ptr == nil {
return aligned_alloc(a, alignment, size, loc)
}
if size == 0 {
free(a, ptr, loc)
return nil
}
if size > MAX_ALLOC_SIZE {
return nil
}
assert(is_power_of_two(alignment))
assert(has_alignment(uintptr(ptr), alignment), "realloc on different alignment than original allocation", loc=loc)
size := size
size = validate_alloc_size(size)
region := (^Region)(uintptr(ptr) - size_of(uint))
// Attempt an in-place resize.
if attempt_region_resize(a, region, size + REGION_HEADER_SIZE) {
return ptr
}
// Can't do it in-place, allocate new region and copy over.
newptr := aligned_alloc(a, alignment, size, loc)
if newptr != nil {
intrinsics.mem_copy(newptr, ptr, min(size, region.size - REGION_HEADER_SIZE))
free(a, ptr, loc=loc)
}
return newptr
}
+10 -1
View File
@@ -64,7 +64,16 @@ Darwin)
fi
fi
CXXFLAGS="$CXXFLAGS $($LLVM_CONFIG --cxxflags --ldflags)"
darwin_sysroot=
if [ $(which xcode-select) ]; then
darwin_sysroot="--sysroot $(xcode-select -p)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"
elif [[ -e "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" ]]; then
darwin_sysroot="--sysroot /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
else
echo "Warning: MacOSX.sdk not found."
fi
CXXFLAGS="$CXXFLAGS $($LLVM_CONFIG --cxxflags --ldflags) ${darwin_sysroot}"
LDFLAGS="$LDFLAGS -liconv -ldl -framework System -lLLVM"
;;
FreeBSD)
+3 -6
View File
@@ -5,13 +5,10 @@ The implementation is non-intrusive, and non-recursive.
*/
package container_avl
import "base:intrinsics"
import "base:runtime"
@(require) import "base:intrinsics"
@(require) import "base:runtime"
import "core:slice"
_ :: intrinsics
_ :: runtime
// Originally based on the CC0 implementation by Eric Biggers
// See: https://github.com/ebiggers/avl_tree/
@@ -675,4 +672,4 @@ iterator_first :: proc "contextless" (it: ^Iterator($Value)) {
if it._cur != nil {
it._next = node_next_or_prev_in_order(it._cur, it._direction)
}
}
}
+568
View File
@@ -0,0 +1,568 @@
// This package implements a red-black tree
package container_rbtree
@(require) import "base:intrinsics"
@(require) import "base:runtime"
import "core:slice"
// Originally based on the CC0 implementation from literateprograms.org
// But with API design mimicking `core:container/avl` for ease of use.
// Direction specifies the traversal direction for a tree iterator.
Direction :: enum i8 {
// Backward is the in-order backwards direction.
Backward = -1,
// Forward is the in-order forwards direction.
Forward = 1,
}
Ordering :: slice.Ordering
// Tree is a red-black tree
Tree :: struct($Key: typeid, $Value: typeid) {
// user_data is a parameter that will be passed to the on_remove
// callback.
user_data: rawptr,
// on_remove is an optional callback that can be called immediately
// after a node is removed from the tree.
on_remove: proc(key: Key, value: Value, user_data: rawptr),
_root: ^Node(Key, Value),
_node_allocator: runtime.Allocator,
_cmp_fn: proc(Key, Key) -> Ordering,
_size: int,
}
// Node is a red-black tree node.
//
// WARNING: It is unsafe to mutate value if the node is part of a tree
// if doing so will alter the Node's sort position relative to other
// elements in the tree.
Node :: struct($Key: typeid, $Value: typeid) {
key: Key,
value: Value,
_parent: ^Node(Key, Value),
_left: ^Node(Key, Value),
_right: ^Node(Key, Value),
_color: Color,
}
// Might store this in the node pointer in the future, but that'll require a decent amount of rework to pass ^^N instead of ^N
Color :: enum uintptr {Black = 0, Red = 1}
// Iterator is a tree iterator.
//
// WARNING: It is unsafe to modify the tree while iterating, except via
// the iterator_remove method.
Iterator :: struct($Key: typeid, $Value: typeid) {
_tree: ^Tree(Key, Value),
_cur: ^Node(Key, Value),
_next: ^Node(Key, Value),
_direction: Direction,
_called_next: bool,
}
// init initializes a tree.
init :: proc {
init_ordered,
init_cmp,
}
// init_cmp initializes a tree.
init_cmp :: proc(t: ^$T/Tree($Key, $Value), cmp_fn: proc(a, b: Key) -> Ordering, node_allocator := context.allocator) {
t._root = nil
t._node_allocator = node_allocator
t._cmp_fn = cmp_fn
t._size = 0
}
// init_ordered initializes a tree containing ordered keys, with
// a comparison function that results in an ascending order sort.
init_ordered :: proc(t: ^$T/Tree($Key, $Value), node_allocator := context.allocator) where intrinsics.type_is_ordered_numeric(Key) {
init_cmp(t, slice.cmp_proc(Key), node_allocator)
}
// destroy de-initializes a tree.
destroy :: proc(t: ^$T/Tree($Key, $Value), call_on_remove: bool = true) {
iter := iterator(t, .Forward)
for _ in iterator_next(&iter) {
iterator_remove(&iter, call_on_remove)
}
}
len :: proc "contextless" (t: ^$T/Tree($Key, $Value)) -> (node_count: int) {
return t._size
}
// first returns the first node in the tree (in-order) or nil iff
// the tree is empty.
first :: proc "contextless" (t: ^$T/Tree($Key, $Value)) -> ^Node(Key, Value) {
return tree_first_or_last_in_order(t, Direction.Backward)
}
// last returns the last element in the tree (in-order) or nil iff
// the tree is empty.
last :: proc "contextless" (t: ^$T/Tree($Key, $Value)) -> ^Node(Key, Value) {
return tree_first_or_last_in_order(t, Direction.Forward)
}
// find finds the key in the tree, and returns the corresponding node, or nil iff the value is not present.
find :: proc(t: ^$T/Tree($Key, $Value), key: Key) -> (node: ^Node(Key, Value)) {
node = t._root
for node != nil {
switch t._cmp_fn(key, node.key) {
case .Equal: return node
case .Less: node = node._left
case .Greater: node = node._right
}
}
return node
}
// find_value finds the key in the tree, and returns the corresponding value, or nil iff the value is not present.
find_value :: proc(t: ^$T/Tree($Key, $Value), key: Key) -> (value: Value, ok: bool) #optional_ok {
if n := find(t, key); n != nil {
return n.value, true
}
return
}
// find_or_insert attempts to insert the value into the tree, and returns
// the node, a boolean indicating if the value was inserted, and the
// node allocator error if relevant. If the value is already present, the existing node is updated.
find_or_insert :: proc(t: ^$T/Tree($Key, $Value), key: Key, value: Value) -> (n: ^Node(Key, Value), inserted: bool, err: runtime.Allocator_Error) {
n_ptr := &t._root
for n_ptr^ != nil {
n = n_ptr^
switch t._cmp_fn(key, n.key) {
case .Less:
n_ptr = &n._left
case .Greater:
n_ptr = &n._right
case .Equal:
return
}
}
_parent := n
n = new_clone(Node(Key, Value){key=key, value=value, _parent=_parent, _color=.Red}, t._node_allocator) or_return
n_ptr^ = n
insert_case1(t, n)
t._size += 1
return n, true, nil
}
// remove removes a node or value from the tree, and returns true iff the
// removal was successful. While the node's value will be left intact,
// the node itself will be freed via the tree's node allocator.
remove :: proc {
remove_key,
remove_node,
}
// remove_value removes a value from the tree, and returns true iff the
// removal was successful. While the node's key + value will be left intact,
// the node itself will be freed via the tree's node allocator.
remove_key :: proc(t: ^$T/Tree($Key, $Value), key: Key, call_on_remove := true) -> bool {
n := find(t, key)
if n == nil {
return false // Key not found, nothing to do
}
return remove_node(t, n, call_on_remove)
}
// remove_node removes a node from the tree, and returns true iff the
// removal was successful. While the node's key + value will be left intact,
// the node itself will be freed via the tree's node allocator.
remove_node :: proc(t: ^$T/Tree($Key, $Value), node: ^$N/Node(Key, Value), call_on_remove := true) -> (found: bool) {
if node._parent == node || (node._parent == nil && t._root != node) {
return false // Don't touch self-parented or dangling nodes.
}
node := node
if node._left != nil && node._right != nil {
// Copy key + value from predecessor and delete it instead
predecessor := maximum_node(node._left)
node.key = predecessor.key
node.value = predecessor.value
node = predecessor
}
child := node._right == nil ? node._left : node._right
if node_color(node) == .Black {
node._color = node_color(child)
remove_case1(t, node)
}
replace_node(t, node, child)
if node._parent == nil && child != nil {
child._color = .Black // root should be black
}
if call_on_remove && t.on_remove != nil {
t.on_remove(node.key, node.value, t.user_data)
}
free(node, t._node_allocator)
t._size -= 1
return true
}
// iterator returns a tree iterator in the specified direction.
iterator :: proc "contextless" (t: ^$T/Tree($Key, $Value), direction: Direction) -> Iterator(Key, Value) {
it: Iterator(Key, Value)
it._tree = cast(^Tree(Key, Value))t
it._direction = direction
iterator_first(&it)
return it
}
// iterator_from_pos returns a tree iterator in the specified direction,
// spanning the range [pos, last] (inclusive).
iterator_from_pos :: proc "contextless" (t: ^$T/Tree($Key, $Value), pos: ^Node(Key, Value), direction: Direction) -> Iterator(Key, Value) {
it: Iterator(Key, Value)
it._tree = transmute(^Tree(Key, Value))t
it._direction = direction
it._next = nil
it._called_next = false
if it._cur = pos; pos != nil {
it._next = node_next_or_prev_in_order(it._cur, it._direction)
}
return it
}
// iterator_get returns the node currently pointed to by the iterator,
// or nil iff the node has been removed, the tree is empty, or the end
// of the tree has been reached.
iterator_get :: proc "contextless" (it: ^$I/Iterator($Key, $Value)) -> ^Node(Key, Value) {
return it._cur
}
// iterator_remove removes the node currently pointed to by the iterator,
// and returns true iff the removal was successful. Semantics are the
// same as the Tree remove.
iterator_remove :: proc(it: ^$I/Iterator($Key, $Value), call_on_remove: bool = true) -> bool {
if it._cur == nil {
return false
}
ok := remove_node(it._tree, it._cur , call_on_remove)
if ok {
it._cur = nil
}
return ok
}
// iterator_next advances the iterator and returns the (node, true) or
// or (nil, false) iff the end of the tree has been reached.
//
// Note: The first call to iterator_next will return the first node instead
// of advancing the iterator.
iterator_next :: proc "contextless" (it: ^$I/Iterator($Key, $Value)) -> (^Node(Key, Value), bool) {
// This check is needed so that the first element gets returned from
// a brand-new iterator, and so that the somewhat contrived case where
// iterator_remove is called before the first call to iterator_next
// returns the correct value.
if !it._called_next {
it._called_next = true
// There can be the contrived case where iterator_remove is
// called before ever calling iterator_next, which needs to be
// handled as an actual call to next.
//
// If this happens it._cur will be nil, so only return the
// first value, if it._cur is valid.
if it._cur != nil {
return it._cur, true
}
}
if it._next == nil {
return nil, false
}
it._cur = it._next
it._next = node_next_or_prev_in_order(it._cur, it._direction)
return it._cur, true
}
@(private)
tree_first_or_last_in_order :: proc "contextless" (t: ^$T/Tree($Key, $Value), direction: Direction) -> ^Node(Key, Value) {
first, sign := t._root, i8(direction)
if first != nil {
for {
tmp := node_get_child(first, sign)
if tmp == nil {
break
}
first = tmp
}
}
return first
}
@(private)
node_get_child :: #force_inline proc "contextless" (n: ^Node($Key, $Value), sign: i8) -> ^Node(Key, Value) {
if sign < 0 {
return n._left
}
return n._right
}
@(private)
node_next_or_prev_in_order :: proc "contextless" (n: ^Node($Key, $Value), direction: Direction) -> ^Node(Key, Value) {
next, tmp: ^Node(Key, Value)
sign := i8(direction)
if next = node_get_child(n, +sign); next != nil {
for {
tmp = node_get_child(next, -sign)
if tmp == nil {
break
}
next = tmp
}
} else {
tmp, next = n, n._parent
for next != nil && tmp == node_get_child(next, +sign) {
tmp, next = next, next._parent
}
}
return next
}
@(private)
iterator_first :: proc "contextless" (it: ^Iterator($Key, $Value)) {
// This is private because behavior when the user manually calls
// iterator_first followed by iterator_next is unintuitive, since
// the first call to iterator_next MUST return the first node
// instead of advancing so that `for node in iterator_next(&next)`
// works as expected.
switch it._direction {
case .Forward:
it._cur = tree_first_or_last_in_order(it._tree, .Backward)
case .Backward:
it._cur = tree_first_or_last_in_order(it._tree, .Forward)
}
it._next = nil
it._called_next = false
if it._cur != nil {
it._next = node_next_or_prev_in_order(it._cur, it._direction)
}
}
@(private)
grand_parent :: proc(n: ^$N/Node($Key, $Value)) -> (g: ^N) {
return n._parent._parent
}
@(private)
sibling :: proc(n: ^$N/Node($Key, $Value)) -> (s: ^N) {
if n == n._parent._left {
return n._parent._right
} else {
return n._parent._left
}
}
@(private)
uncle :: proc(n: ^$N/Node($Key, $Value)) -> (u: ^N) {
return sibling(n._parent)
}
@(private)
rotate__left :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
r := n._right
replace_node(t, n, r)
n._right = r._left
if r._left != nil {
r._left._parent = n
}
r._left = n
n._parent = r
}
@(private)
rotate__right :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
l := n._left
replace_node(t, n, l)
n._left = l._right
if l._right != nil {
l._right._parent = n
}
l._right = n
n._parent = l
}
@(private)
replace_node :: proc(t: ^$T/Tree($Key, $Value), old_n: ^$N/Node(Key, Value), new_n: ^N) {
if old_n._parent == nil {
t._root = new_n
} else {
if (old_n == old_n._parent._left) {
old_n._parent._left = new_n
} else {
old_n._parent._right = new_n
}
}
if new_n != nil {
new_n._parent = old_n._parent
}
}
@(private)
insert_case1 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
if n._parent == nil {
n._color = .Black
} else {
insert_case2(t, n)
}
}
@(private)
insert_case2 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
if node_color(n._parent) == .Black {
return // Tree is still valid
} else {
insert_case3(t, n)
}
}
@(private)
insert_case3 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
if node_color(uncle(n)) == .Red {
n._parent._color = .Black
uncle(n)._color = .Black
grand_parent(n)._color = .Red
insert_case1(t, grand_parent(n))
} else {
insert_case4(t, n)
}
}
@(private)
insert_case4 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
n := n
if n == n._parent._right && n._parent == grand_parent(n)._left {
rotate__left(t, n._parent)
n = n._left
} else if n == n._parent._left && n._parent == grand_parent(n)._right {
rotate__right(t, n._parent)
n = n._right
}
insert_case5(t, n)
}
@(private)
insert_case5 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
n._parent._color = .Black
grand_parent(n)._color = .Red
if n == n._parent._left && n._parent == grand_parent(n)._left {
rotate__right(t, grand_parent(n))
} else {
rotate__left(t, grand_parent(n))
}
}
// The maximum_node() helper function just walks _right until it reaches the last non-leaf:
@(private)
maximum_node :: proc(n: ^$N/Node($Key, $Value)) -> (max_node: ^N) {
n := n
for n._right != nil {
n = n._right
}
return n
}
@(private)
remove_case1 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
if n._parent == nil {
return
} else {
remove_case2(t, n)
}
}
@(private)
remove_case2 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
if node_color(sibling(n)) == .Red {
n._parent._color = .Red
sibling(n)._color = .Black
if n == n._parent._left {
rotate__left(t, n._parent)
} else {
rotate__right(t, n._parent)
}
}
remove_case3(t, n)
}
@(private)
remove_case3 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
if node_color(n._parent) == .Black &&
node_color(sibling(n)) == .Black &&
node_color(sibling(n)._left) == .Black &&
node_color(sibling(n)._right) == .Black {
sibling(n)._color = .Red
remove_case1(t, n._parent)
} else {
remove_case4(t, n)
}
}
@(private)
remove_case4 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
if node_color(n._parent) == .Red &&
node_color(sibling(n)) == .Black &&
node_color(sibling(n)._left) == .Black &&
node_color(sibling(n)._right) == .Black {
sibling(n)._color = .Red
n._parent._color = .Black
} else {
remove_case5(t, n)
}
}
@(private)
remove_case5 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
if n == n._parent._left &&
node_color(sibling(n)) == .Black &&
node_color(sibling(n)._left) == .Red &&
node_color(sibling(n)._right) == .Black {
sibling(n)._color = .Red
sibling(n)._left._color = .Black
rotate__right(t, sibling(n))
} else if n == n._parent._right &&
node_color(sibling(n)) == .Black &&
node_color(sibling(n)._right) == .Red &&
node_color(sibling(n)._left) == .Black {
sibling(n)._color = .Red
sibling(n)._right._color = .Black
rotate__left(t, sibling(n))
}
remove_case6(t, n)
}
@(private)
remove_case6 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
sibling(n)._color = node_color(n._parent)
n._parent._color = .Black
if n == n._parent._left {
sibling(n)._right._color = .Black
rotate__left(t, n._parent)
} else {
sibling(n)._left._color = .Black
rotate__right(t, n._parent)
}
}
node_color :: proc(n: ^$N/Node($Key, $Value)) -> (c: Color) {
return n == nil ? .Black : n._color
}
+88
View File
@@ -0,0 +1,88 @@
//+build ignore
package encoding_csv
import "core:fmt"
import "core:encoding/csv"
import "core:os"
// Requires keeping the entire CSV file in memory at once
iterate_csv_from_string :: proc(filename: string) {
r: csv.Reader
r.trim_leading_space = true
r.reuse_record = true // Without it you have to delete(record)
r.reuse_record_buffer = true // Without it you have to each of the fields within it
defer csv.reader_destroy(&r)
if csv_data, ok := os.read_entire_file(filename); ok {
csv.reader_init_with_string(&r, string(csv_data))
defer delete(csv_data)
} else {
fmt.printfln("Unable to open file: %v", filename)
return
}
for r, i, err in csv.iterator_next(&r) {
if err != nil { /* Do something with error */ }
for f, j in r {
fmt.printfln("Record %v, field %v: %q", i, j, f)
}
}
}
// Reads the CSV as it's processed (with a small buffer)
iterate_csv_from_stream :: proc(filename: string) {
fmt.printfln("Hellope from %v", filename)
r: csv.Reader
r.trim_leading_space = true
r.reuse_record = true // Without it you have to delete(record)
r.reuse_record_buffer = true // Without it you have to each of the fields within it
defer csv.reader_destroy(&r)
handle, errno := os.open(filename)
if errno != os.ERROR_NONE {
fmt.printfln("Error opening file: %v", filename)
return
}
defer os.close(handle)
csv.reader_init(&r, os.stream_from_handle(handle))
for r, i in csv.iterator_next(&r) {
for f, j in r {
fmt.printfln("Record %v, field %v: %q", i, j, f)
}
}
fmt.printfln("Error: %v", csv.iterator_last_error(r))
}
// Read all records at once
read_csv_from_string :: proc(filename: string) {
r: csv.Reader
r.trim_leading_space = true
r.reuse_record = true // Without it you have to delete(record)
r.reuse_record_buffer = true // Without it you have to each of the fields within it
defer csv.reader_destroy(&r)
if csv_data, ok := os.read_entire_file(filename); ok {
csv.reader_init_with_string(&r, string(csv_data))
defer delete(csv_data)
} else {
fmt.printfln("Unable to open file: %v", filename)
return
}
records, err := csv.read_all(&r)
if err != nil { /* Do something with CSV parse error */ }
defer {
for rec in records {
delete(rec)
}
delete(records)
}
for r, i in records {
for f, j in r {
fmt.printfln("Record %v, field %v: %q", i, j, f)
}
}
}
+23 -2
View File
@@ -57,6 +57,9 @@ Reader :: struct {
field_indices: [dynamic]int,
last_record: [dynamic]string,
sr: strings.Reader, // used by reader_init_with_string
// Set and used by the iterator. Query using `iterator_last_error`
last_iterator_error: Error,
}
@@ -121,6 +124,25 @@ reader_destroy :: proc(r: ^Reader) {
bufio.reader_destroy(&r.r)
}
/*
Returns a record at a time.
for record, row_idx in csv.iterator_next(&r) { ... }
TIP: If you process the results within the loop and don't need to own the results,
you can set the Reader's `reuse_record` and `reuse_record_reuse_record_buffer` to true;
you won't need to delete the record or its fields.
*/
iterator_next :: proc(r: ^Reader) -> (record: []string, idx: int, err: Error, more: bool) {
record, r.last_iterator_error = read(r)
return record, r.line_count - 1, r.last_iterator_error, r.last_iterator_error == nil
}
// Get last error if we the iterator
iterator_last_error :: proc(r: Reader) -> (err: Error) {
return r.last_iterator_error
}
// read reads a single record (a slice of fields) from r
//
// All \r\n sequences are normalized to \n, including multi-line field
@@ -460,5 +482,4 @@ _read_record :: proc(r: ^Reader, dst: ^[dynamic]string, allocator := context.all
r.fields_per_record = len(dst)
}
return dst[:], err
}
}
-2
View File
@@ -539,8 +539,6 @@ marshal_to_writer :: proc(w: io.Writer, v: any, opt: ^Marshal_Options) -> (err:
case: panic("unknown bit_size size")
}
io.write_u64(w, bit_data) or_return
return .Unsupported_Type
}
return
+148 -54
View File
@@ -13,20 +13,7 @@ import "core:unicode/utf8"
// Internal data structure that stores the required information for formatted printing
Info :: struct {
minus: bool,
plus: bool,
space: bool,
zero: bool,
hash: bool,
width_set: bool,
prec_set: bool,
width: int,
prec: int,
indent: int,
ignore_user_formatters: bool,
in_bad: bool,
using state: Info_State,
writer: io.Writer,
arg: any, // Temporary
@@ -39,6 +26,24 @@ Info :: struct {
n: int, // bytes written
}
Info_State :: struct {
minus: bool,
plus: bool,
space: bool,
zero: bool,
hash: bool,
width_set: bool,
prec_set: bool,
ignore_user_formatters: bool,
in_bad: bool,
width: int,
prec: int,
indent: int,
}
// Custom formatter signature. It returns true if the formatting was successful and false when it could not be done
User_Formatter :: #type proc(fi: ^Info, arg: any, verb: rune) -> bool
@@ -994,6 +999,33 @@ _fmt_int :: proc(fi: ^Info, u: u64, base: int, is_signed: bool, bit_size: int, d
}
}
buf: [256]byte
start := 0
if fi.hash && !is_signed {
switch base {
case 2:
io.write_byte(fi.writer, '0', &fi.n)
io.write_byte(fi.writer, 'b', &fi.n)
start = 2
case 8:
io.write_byte(fi.writer, '0', &fi.n)
io.write_byte(fi.writer, 'o', &fi.n)
start = 2
case 12:
io.write_byte(fi.writer, '0', &fi.n)
io.write_byte(fi.writer, 'o', &fi.n)
start = 2
case 16:
io.write_byte(fi.writer, '0', &fi.n)
io.write_byte(fi.writer, 'x', &fi.n)
start = 2
}
}
prec := 0
if fi.prec_set {
prec = fi.prec
@@ -1019,14 +1051,10 @@ _fmt_int :: proc(fi: ^Info, u: u64, base: int, is_signed: bool, bit_size: int, d
panic("_fmt_int: unknown base, whoops")
}
buf: [256]byte
start := 0
flags: strconv.Int_Flags
if fi.hash { flags |= {.Prefix} }
if fi.plus { flags |= {.Plus} }
if fi.hash && !fi.zero && start == 0 { flags |= {.Prefix} }
if fi.plus { flags |= {.Plus} }
s := strconv.append_bits(buf[start:], u, base, is_signed, bit_size, digits, flags)
prev_zero := fi.zero
defer fi.zero = prev_zero
fi.zero = false
@@ -1056,6 +1084,33 @@ _fmt_int_128 :: proc(fi: ^Info, u: u128, base: int, is_signed: bool, bit_size: i
}
}
buf: [256]byte
start := 0
if fi.hash && !is_signed {
switch base {
case 2:
io.write_byte(fi.writer, '0', &fi.n)
io.write_byte(fi.writer, 'b', &fi.n)
start = 2
case 8:
io.write_byte(fi.writer, '0', &fi.n)
io.write_byte(fi.writer, 'o', &fi.n)
start = 2
case 12:
io.write_byte(fi.writer, '0', &fi.n)
io.write_byte(fi.writer, 'o', &fi.n)
start = 2
case 16:
io.write_byte(fi.writer, '0', &fi.n)
io.write_byte(fi.writer, 'x', &fi.n)
start = 2
}
}
prec := 0
if fi.prec_set {
prec = fi.prec
@@ -1081,12 +1136,9 @@ _fmt_int_128 :: proc(fi: ^Info, u: u128, base: int, is_signed: bool, bit_size: i
panic("_fmt_int: unknown base, whoops")
}
buf: [256]byte
start := 0
flags: strconv.Int_Flags
if fi.hash && !fi.zero { flags |= {.Prefix} }
if fi.plus { flags |= {.Plus} }
if fi.hash && !fi.zero && start == 0 { flags |= {.Prefix} }
if fi.plus { flags |= {.Plus} }
s := strconv.append_bits_128(buf[start:], u, base, is_signed, bit_size, digits, flags)
if fi.hash && fi.zero && fi.indent == 0 {
@@ -1777,7 +1829,7 @@ fmt_write_array :: proc(fi: ^Info, array_data: rawptr, count: int, elem_size: in
// Returns: A boolean value indicating whether to continue processing the tag
//
@(private)
handle_tag :: proc(data: rawptr, info: reflect.Type_Info_Struct, idx: int, verb: ^rune, optional_len: ^int, use_nul_termination: ^bool) -> (do_continue: bool) {
handle_tag :: proc(state: ^Info_State, data: rawptr, info: reflect.Type_Info_Struct, idx: int, verb: ^rune, optional_len: ^int, use_nul_termination: ^bool) -> (do_continue: bool) {
handle_optional_len :: proc(data: rawptr, info: reflect.Type_Info_Struct, field_name: string, optional_len: ^int) {
if optional_len == nil {
return
@@ -1794,45 +1846,83 @@ handle_tag :: proc(data: rawptr, info: reflect.Type_Info_Struct, idx: int, verb:
break
}
}
tag := info.tags[idx]
if vt, ok := reflect.struct_tag_lookup(reflect.Struct_Tag(tag), "fmt"); ok {
value := strings.trim_space(string(vt))
switch value {
case "": return false
case "": return false
case "-": return true
}
r, w := utf8.decode_rune_in_string(value)
value = value[w:]
if value == "" || value[0] == ',' {
if verb^ == 'w' {
// TODO(bill): is this a good idea overriding that field tags if 'w' is used?
switch r {
case 's': r = 'q'
case: r = 'w'
}
fi := state
head, _, tail := strings.partition(value, ",")
i := 0
prefix_loop: for ; i < len(head); i += 1 {
switch head[i] {
case '+':
fi.plus = true
case '-':
fi.minus = true
fi.zero = false
case ' ':
fi.space = true
case '#':
fi.hash = true
case '0':
fi.zero = !fi.minus
case:
break prefix_loop
}
verb^ = r
if len(value) > 0 && value[0] == ',' {
field_name := value[1:]
if field_name == "0" {
if use_nul_termination != nil {
use_nul_termination^ = true
}
} else {
switch r {
case 's', 'q':
}
fi.width, i, fi.width_set = _parse_int(head, i)
if i < len(head) && head[i] == '.' {
i += 1
prev_i := i
fi.prec, i, fi.prec_set = _parse_int(head, i)
if i == prev_i {
fi.prec = 0
fi.prec_set = true
}
}
r: rune
if i >= len(head) || head[i] == ' ' {
r = 'v'
} else {
r, _ = utf8.decode_rune_in_string(head[i:])
}
if verb^ == 'w' {
// TODO(bill): is this a good idea overriding that field tags if 'w' is used?
switch r {
case 's': r = 'q'
case: r = 'w'
}
}
verb^ = r
if tail != "" {
field_name := tail
if field_name == "0" {
if use_nul_termination != nil {
use_nul_termination^ = true
}
} else {
switch r {
case 's', 'q':
handle_optional_len(data, info, field_name, optional_len)
case 'v', 'w':
#partial switch reflect.type_kind(info.types[idx].id) {
case .String, .Multi_Pointer, .Array, .Slice, .Dynamic_Array:
handle_optional_len(data, info, field_name, optional_len)
case 'v', 'w':
#partial switch reflect.type_kind(info.types[idx].id) {
case .String, .Multi_Pointer, .Array, .Slice, .Dynamic_Array:
handle_optional_len(data, info, field_name, optional_len)
}
}
}
}
}
}
return false
return
}
// Formats a struct for output, handling various struct types (e.g., SOA, raw unions)
//
@@ -1980,7 +2070,9 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
optional_len: int = -1
use_nul_termination: bool = false
verb := the_verb if the_verb == 'w' else 'v'
if handle_tag(v.data, info, i, &verb, &optional_len, &use_nul_termination) {
new_state := fi.state
if handle_tag(&new_state, v.data, info, i, &verb, &optional_len, &use_nul_termination) {
continue
}
field_count += 1
@@ -2005,8 +2097,11 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
if t := info.types[i]; reflect.is_any(t) {
io.write_string(fi.writer, "any{}", &fi.n)
} else {
prev_state := fi.state
fi.state = new_state
data := rawptr(uintptr(v.data) + info.offsets[i])
fmt_arg(fi, any{data, t.id}, verb)
fi.state = prev_state
}
if do_trailing_comma { io.write_string(fi.writer, ",\n", &fi.n) }
@@ -2679,7 +2774,6 @@ fmt_value :: proc(fi: ^Info, v: any, verb: rune) {
io.write_byte(fi.writer, '[' if verb != 'w' else '{', &fi.n)
io.write_byte(fi.writer, '\n', &fi.n)
defer {
io.write_byte(fi.writer, '\n', &fi.n)
fmt_write_indent(fi)
io.write_byte(fi.writer, ']' if verb != 'w' else '}', &fi.n)
}
+377 -380
View File
@@ -1,381 +1,378 @@
/*
Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
Made available under Odin's BSD-3 license.
List of contributors:
Jeroen van Rijn: Initial implementation.
*/
// package qoi implements a QOI image reader
//
// The QOI specification is at https://qoiformat.org.
package qoi
import "core:image"
import "core:compress"
import "core:bytes"
Error :: image.Error
Image :: image.Image
Options :: image.Options
RGB_Pixel :: image.RGB_Pixel
RGBA_Pixel :: image.RGBA_Pixel
save_to_buffer :: proc(output: ^bytes.Buffer, img: ^Image, options := Options{}, allocator := context.allocator) -> (err: Error) {
context.allocator = allocator
if img == nil {
return .Invalid_Input_Image
}
if output == nil {
return .Invalid_Output
}
pixels := img.width * img.height
if pixels == 0 || pixels > image.MAX_DIMENSIONS {
return .Invalid_Input_Image
}
// QOI supports only 8-bit images with 3 or 4 channels.
if img.depth != 8 || img.channels < 3 || img.channels > 4 {
return .Invalid_Input_Image
}
if img.channels * pixels != len(img.pixels.buf) {
return .Invalid_Input_Image
}
written := 0
// Calculate and allocate maximum size. We'll reclaim space to actually written output at the end.
max_size := pixels * (img.channels + 1) + size_of(image.QOI_Header) + size_of(u64be)
if resize(&output.buf, max_size) != nil {
return .Unable_To_Allocate_Or_Resize
}
header := image.QOI_Header{
magic = image.QOI_Magic,
width = u32be(img.width),
height = u32be(img.height),
channels = u8(img.channels),
color_space = .Linear if .qoi_all_channels_linear in options else .sRGB,
}
header_bytes := transmute([size_of(image.QOI_Header)]u8)header
copy(output.buf[written:], header_bytes[:])
written += size_of(image.QOI_Header)
/*
Encode loop starts here.
*/
seen: [64]RGBA_Pixel
pix := RGBA_Pixel{0, 0, 0, 255}
prev := pix
seen[qoi_hash(pix)] = pix
input := img.pixels.buf[:]
run := u8(0)
for len(input) > 0 {
if img.channels == 4 {
pix = (^RGBA_Pixel)(raw_data(input))^
} else {
pix.rgb = (^RGB_Pixel)(raw_data(input))^
}
input = input[img.channels:]
if pix == prev {
run += 1
// As long as the pixel matches the last one, accumulate the run total.
// If we reach the max run length or the end of the image, write the run.
if run == 62 || len(input) == 0 {
// Encode and write run
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
written += 1
run = 0
}
} else {
if run > 0 {
// The pixel differs from the previous one, but we still need to write the pending run.
// Encode and write run
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
written += 1
run = 0
}
index := qoi_hash(pix)
if seen[index] == pix {
// Write indexed pixel
output.buf[written] = u8(QOI_Opcode_Tag.INDEX) | index
written += 1
} else {
// Add pixel to index
seen[index] = pix
// If the alpha matches the previous pixel's alpha, we don't need to write a full RGBA literal.
if pix.a == prev.a {
// Delta
d := pix.rgb - prev.rgb
// DIFF, biased and modulo 256
_d := d + 2
// LUMA, biased and modulo 256
_l := RGB_Pixel{ d.r - d.g + 8, d.g + 32, d.b - d.g + 8 }
if _d.r < 4 && _d.g < 4 && _d.b < 4 {
// Delta is between -2 and 1 inclusive
output.buf[written] = u8(QOI_Opcode_Tag.DIFF) | _d.r << 4 | _d.g << 2 | _d.b
written += 1
} else if _l.r < 16 && _l.g < 64 && _l.b < 16 {
// Biased luma is between {-8..7, -32..31, -8..7}
output.buf[written ] = u8(QOI_Opcode_Tag.LUMA) | _l.g
output.buf[written + 1] = _l.r << 4 | _l.b
written += 2
} else {
// Write RGB literal
output.buf[written] = u8(QOI_Opcode_Tag.RGB)
pix_bytes := transmute([4]u8)pix
copy(output.buf[written + 1:], pix_bytes[:3])
written += 4
}
} else {
// Write RGBA literal
output.buf[written] = u8(QOI_Opcode_Tag.RGBA)
pix_bytes := transmute([4]u8)pix
copy(output.buf[written + 1:], pix_bytes[:])
written += 5
}
}
}
prev = pix
}
trailer := []u8{0, 0, 0, 0, 0, 0, 0, 1}
copy(output.buf[written:], trailer[:])
written += len(trailer)
resize(&output.buf, written)
return nil
}
load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
ctx := &compress.Context_Memory_Input{
input_data = data,
}
img, err = load_from_context(ctx, options, allocator)
return img, err
}
@(optimization_mode="speed")
load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
context.allocator = allocator
options := options
if .info in options {
options |= {.return_metadata, .do_not_decompress_image}
options -= {.info}
}
if .return_header in options && .return_metadata in options {
options -= {.return_header}
}
header := image.read_data(ctx, image.QOI_Header) or_return
if header.magic != image.QOI_Magic {
return img, .Invalid_Signature
}
if img == nil {
img = new(Image)
}
img.which = .QOI
if .return_metadata in options {
info := new(image.QOI_Info)
info.header = header
img.metadata = info
}
if header.channels != 3 && header.channels != 4 {
return img, .Invalid_Number_Of_Channels
}
if header.color_space != .sRGB && header.color_space != .Linear {
return img, .Invalid_Color_Space
}
if header.width == 0 || header.height == 0 {
return img, .Invalid_Image_Dimensions
}
total_pixels := header.width * header.height
if total_pixels > image.MAX_DIMENSIONS {
return img, .Image_Dimensions_Too_Large
}
img.width = int(header.width)
img.height = int(header.height)
img.channels = 4 if .alpha_add_if_missing in options else int(header.channels)
img.depth = 8
if .do_not_decompress_image in options {
img.channels = int(header.channels)
return
}
bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), img.channels, 8)
if resize(&img.pixels.buf, bytes_needed) != nil {
return img, .Unable_To_Allocate_Or_Resize
}
/*
Decode loop starts here.
*/
seen: [64]RGBA_Pixel
pix := RGBA_Pixel{0, 0, 0, 255}
seen[qoi_hash(pix)] = pix
pixels := img.pixels.buf[:]
decode: for len(pixels) > 0 {
data := image.read_u8(ctx) or_return
tag := QOI_Opcode_Tag(data)
#partial switch tag {
case .RGB:
pix.rgb = image.read_data(ctx, RGB_Pixel) or_return
#no_bounds_check {
seen[qoi_hash(pix)] = pix
}
case .RGBA:
pix = image.read_data(ctx, RGBA_Pixel) or_return
#no_bounds_check {
seen[qoi_hash(pix)] = pix
}
case:
// 2-bit tag
tag = QOI_Opcode_Tag(data & QOI_Opcode_Mask)
#partial switch tag {
case .INDEX:
pix = seen[data & 63]
case .DIFF:
diff_r := ((data >> 4) & 3) - 2
diff_g := ((data >> 2) & 3) - 2
diff_b := ((data >> 0) & 3) - 2
pix += {diff_r, diff_g, diff_b, 0}
#no_bounds_check {
seen[qoi_hash(pix)] = pix
}
case .LUMA:
data2 := image.read_u8(ctx) or_return
diff_g := (data & 63) - 32
diff_r := diff_g - 8 + ((data2 >> 4) & 15)
diff_b := diff_g - 8 + (data2 & 15)
pix += {diff_r, diff_g, diff_b, 0}
#no_bounds_check {
seen[qoi_hash(pix)] = pix
}
case .RUN:
if length := int(data & 63) + 1; (length * img.channels) > len(pixels) {
return img, .Corrupt
} else {
#no_bounds_check for _ in 0..<length {
copy(pixels, pix[:img.channels])
pixels = pixels[img.channels:]
}
}
continue decode
case:
unreachable()
}
}
#no_bounds_check {
copy(pixels, pix[:img.channels])
pixels = pixels[img.channels:]
}
}
// The byte stream's end is marked with 7 0x00 bytes followed by a single 0x01 byte.
trailer, trailer_err := compress.read_data(ctx, u64be)
if trailer_err != nil || trailer != 0x1 {
return img, .Missing_Or_Corrupt_Trailer
}
if .alpha_premultiply in options && !image.alpha_drop_if_present(img, options) {
return img, .Post_Processing_Error
}
return
}
/*
Cleanup of image-specific data.
*/
destroy :: proc(img: ^Image) {
if img == nil {
/*
Nothing to do.
Load must've returned with an error.
*/
return
}
bytes.buffer_destroy(&img.pixels)
if v, ok := img.metadata.(^image.QOI_Info); ok {
free(v)
}
free(img)
}
QOI_Opcode_Tag :: enum u8 {
// 2-bit tags
INDEX = 0b0000_0000, // 6-bit index into color array follows
DIFF = 0b0100_0000, // 3x (RGB) 2-bit difference follows (-2..1), bias of 2.
LUMA = 0b1000_0000, // Luma difference
RUN = 0b1100_0000, // Run length encoding, bias -1
// 8-bit tags
RGB = 0b1111_1110, // Raw RGB pixel follows
RGBA = 0b1111_1111, // Raw RGBA pixel follows
}
QOI_Opcode_Mask :: 0b1100_0000
QOI_Data_Mask :: 0b0011_1111
qoi_hash :: #force_inline proc(pixel: RGBA_Pixel) -> (index: u8) {
i1 := u16(pixel.r) * 3
i2 := u16(pixel.g) * 5
i3 := u16(pixel.b) * 7
i4 := u16(pixel.a) * 11
return u8((i1 + i2 + i3 + i4) & 63)
}
@(init, private)
_register :: proc() {
image.register(.QOI, load_from_bytes, destroy)
/*
Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
Made available under Odin's BSD-3 license.
List of contributors:
Jeroen van Rijn: Initial implementation.
*/
// package qoi implements a QOI image reader
//
// The QOI specification is at https://qoiformat.org.
package qoi
import "core:image"
import "core:compress"
import "core:bytes"
Error :: image.Error
Image :: image.Image
Options :: image.Options
RGB_Pixel :: image.RGB_Pixel
RGBA_Pixel :: image.RGBA_Pixel
save_to_buffer :: proc(output: ^bytes.Buffer, img: ^Image, options := Options{}, allocator := context.allocator) -> (err: Error) {
context.allocator = allocator
if img == nil {
return .Invalid_Input_Image
}
if output == nil {
return .Invalid_Output
}
pixels := img.width * img.height
if pixels == 0 || pixels > image.MAX_DIMENSIONS {
return .Invalid_Input_Image
}
// QOI supports only 8-bit images with 3 or 4 channels.
if img.depth != 8 || img.channels < 3 || img.channels > 4 {
return .Invalid_Input_Image
}
if img.channels * pixels != len(img.pixels.buf) {
return .Invalid_Input_Image
}
written := 0
// Calculate and allocate maximum size. We'll reclaim space to actually written output at the end.
max_size := pixels * (img.channels + 1) + size_of(image.QOI_Header) + size_of(u64be)
if resize(&output.buf, max_size) != nil {
return .Unable_To_Allocate_Or_Resize
}
header := image.QOI_Header{
magic = image.QOI_Magic,
width = u32be(img.width),
height = u32be(img.height),
channels = u8(img.channels),
color_space = .Linear if .qoi_all_channels_linear in options else .sRGB,
}
header_bytes := transmute([size_of(image.QOI_Header)]u8)header
copy(output.buf[written:], header_bytes[:])
written += size_of(image.QOI_Header)
/*
Encode loop starts here.
*/
seen: [64]RGBA_Pixel
pix := RGBA_Pixel{0, 0, 0, 255}
prev := pix
input := img.pixels.buf[:]
run := u8(0)
for len(input) > 0 {
if img.channels == 4 {
pix = (^RGBA_Pixel)(raw_data(input))^
} else {
pix.rgb = (^RGB_Pixel)(raw_data(input))^
}
input = input[img.channels:]
if pix == prev {
run += 1
// As long as the pixel matches the last one, accumulate the run total.
// If we reach the max run length or the end of the image, write the run.
if run == 62 || len(input) == 0 {
// Encode and write run
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
written += 1
run = 0
}
} else {
if run > 0 {
// The pixel differs from the previous one, but we still need to write the pending run.
// Encode and write run
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
written += 1
run = 0
}
index := qoi_hash(pix)
if seen[index] == pix {
// Write indexed pixel
output.buf[written] = u8(QOI_Opcode_Tag.INDEX) | index
written += 1
} else {
// Add pixel to index
seen[index] = pix
// If the alpha matches the previous pixel's alpha, we don't need to write a full RGBA literal.
if pix.a == prev.a {
// Delta
d := pix.rgb - prev.rgb
// DIFF, biased and modulo 256
_d := d + 2
// LUMA, biased and modulo 256
_l := RGB_Pixel{ d.r - d.g + 8, d.g + 32, d.b - d.g + 8 }
if _d.r < 4 && _d.g < 4 && _d.b < 4 {
// Delta is between -2 and 1 inclusive
output.buf[written] = u8(QOI_Opcode_Tag.DIFF) | _d.r << 4 | _d.g << 2 | _d.b
written += 1
} else if _l.r < 16 && _l.g < 64 && _l.b < 16 {
// Biased luma is between {-8..7, -32..31, -8..7}
output.buf[written ] = u8(QOI_Opcode_Tag.LUMA) | _l.g
output.buf[written + 1] = _l.r << 4 | _l.b
written += 2
} else {
// Write RGB literal
output.buf[written] = u8(QOI_Opcode_Tag.RGB)
pix_bytes := transmute([4]u8)pix
copy(output.buf[written + 1:], pix_bytes[:3])
written += 4
}
} else {
// Write RGBA literal
output.buf[written] = u8(QOI_Opcode_Tag.RGBA)
pix_bytes := transmute([4]u8)pix
copy(output.buf[written + 1:], pix_bytes[:])
written += 5
}
}
}
prev = pix
}
trailer := []u8{0, 0, 0, 0, 0, 0, 0, 1}
copy(output.buf[written:], trailer[:])
written += len(trailer)
resize(&output.buf, written)
return nil
}
load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
ctx := &compress.Context_Memory_Input{
input_data = data,
}
img, err = load_from_context(ctx, options, allocator)
return img, err
}
@(optimization_mode="speed")
load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
context.allocator = allocator
options := options
if .info in options {
options |= {.return_metadata, .do_not_decompress_image}
options -= {.info}
}
if .return_header in options && .return_metadata in options {
options -= {.return_header}
}
header := image.read_data(ctx, image.QOI_Header) or_return
if header.magic != image.QOI_Magic {
return img, .Invalid_Signature
}
if img == nil {
img = new(Image)
}
img.which = .QOI
if .return_metadata in options {
info := new(image.QOI_Info)
info.header = header
img.metadata = info
}
if header.channels != 3 && header.channels != 4 {
return img, .Invalid_Number_Of_Channels
}
if header.color_space != .sRGB && header.color_space != .Linear {
return img, .Invalid_Color_Space
}
if header.width == 0 || header.height == 0 {
return img, .Invalid_Image_Dimensions
}
total_pixels := header.width * header.height
if total_pixels > image.MAX_DIMENSIONS {
return img, .Image_Dimensions_Too_Large
}
img.width = int(header.width)
img.height = int(header.height)
img.channels = 4 if .alpha_add_if_missing in options else int(header.channels)
img.depth = 8
if .do_not_decompress_image in options {
img.channels = int(header.channels)
return
}
bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), img.channels, 8)
if resize(&img.pixels.buf, bytes_needed) != nil {
return img, .Unable_To_Allocate_Or_Resize
}
/*
Decode loop starts here.
*/
seen: [64]RGBA_Pixel
pix := RGBA_Pixel{0, 0, 0, 255}
pixels := img.pixels.buf[:]
decode: for len(pixels) > 0 {
data := image.read_u8(ctx) or_return
tag := QOI_Opcode_Tag(data)
#partial switch tag {
case .RGB:
pix.rgb = image.read_data(ctx, RGB_Pixel) or_return
#no_bounds_check {
seen[qoi_hash(pix)] = pix
}
case .RGBA:
pix = image.read_data(ctx, RGBA_Pixel) or_return
#no_bounds_check {
seen[qoi_hash(pix)] = pix
}
case:
// 2-bit tag
tag = QOI_Opcode_Tag(data & QOI_Opcode_Mask)
#partial switch tag {
case .INDEX:
pix = seen[data & 63]
case .DIFF:
diff_r := ((data >> 4) & 3) - 2
diff_g := ((data >> 2) & 3) - 2
diff_b := ((data >> 0) & 3) - 2
pix += {diff_r, diff_g, diff_b, 0}
#no_bounds_check {
seen[qoi_hash(pix)] = pix
}
case .LUMA:
data2 := image.read_u8(ctx) or_return
diff_g := (data & 63) - 32
diff_r := diff_g - 8 + ((data2 >> 4) & 15)
diff_b := diff_g - 8 + (data2 & 15)
pix += {diff_r, diff_g, diff_b, 0}
#no_bounds_check {
seen[qoi_hash(pix)] = pix
}
case .RUN:
if length := int(data & 63) + 1; (length * img.channels) > len(pixels) {
return img, .Corrupt
} else {
#no_bounds_check for _ in 0..<length {
copy(pixels, pix[:img.channels])
pixels = pixels[img.channels:]
}
}
continue decode
case:
unreachable()
}
}
#no_bounds_check {
copy(pixels, pix[:img.channels])
pixels = pixels[img.channels:]
}
}
// The byte stream's end is marked with 7 0x00 bytes followed by a single 0x01 byte.
trailer, trailer_err := compress.read_data(ctx, u64be)
if trailer_err != nil || trailer != 0x1 {
return img, .Missing_Or_Corrupt_Trailer
}
if .alpha_premultiply in options && !image.alpha_drop_if_present(img, options) {
return img, .Post_Processing_Error
}
return
}
/*
Cleanup of image-specific data.
*/
destroy :: proc(img: ^Image) {
if img == nil {
/*
Nothing to do.
Load must've returned with an error.
*/
return
}
bytes.buffer_destroy(&img.pixels)
if v, ok := img.metadata.(^image.QOI_Info); ok {
free(v)
}
free(img)
}
QOI_Opcode_Tag :: enum u8 {
// 2-bit tags
INDEX = 0b0000_0000, // 6-bit index into color array follows
DIFF = 0b0100_0000, // 3x (RGB) 2-bit difference follows (-2..1), bias of 2.
LUMA = 0b1000_0000, // Luma difference
RUN = 0b1100_0000, // Run length encoding, bias -1
// 8-bit tags
RGB = 0b1111_1110, // Raw RGB pixel follows
RGBA = 0b1111_1111, // Raw RGBA pixel follows
}
QOI_Opcode_Mask :: 0b1100_0000
QOI_Data_Mask :: 0b0011_1111
qoi_hash :: #force_inline proc(pixel: RGBA_Pixel) -> (index: u8) {
i1 := u16(pixel.r) * 3
i2 := u16(pixel.g) * 5
i3 := u16(pixel.b) * 7
i4 := u16(pixel.a) * 11
return u8((i1 + i2 + i3 + i4) & 63)
}
@(init, private)
_register :: proc() {
image.register(.QOI, load_from_bytes, destroy)
}
+3
View File
@@ -0,0 +1,3 @@
package odin_format
#panic("The format package has been deprecated. Please look at https://github.com/DanielGavin/ols")
-41
View File
@@ -1,41 +0,0 @@
package odin_format
import "core:odin/printer"
import "core:odin/parser"
import "core:odin/ast"
default_style := printer.default_style
simplify :: proc(file: ^ast.File) {
}
format :: proc(filepath: string, source: string, config: printer.Config, parser_flags := parser.Flags{}, allocator := context.allocator) -> (string, bool) {
config := config
pkg := ast.Package {
kind = .Normal,
}
file := ast.File {
pkg = &pkg,
src = source,
fullpath = filepath,
}
config.newline_limit = clamp(config.newline_limit, 0, 16)
config.spaces = clamp(config.spaces, 1, 16)
config.align_length_break = clamp(config.align_length_break, 0, 64)
p := parser.default_parser(parser_flags)
ok := parser.parse_file(&p, &file)
if !ok || file.syntax_error_count > 0 {
return {}, false
}
prnt := printer.make_printer(config, allocator)
return printer.print(&prnt, &file), true
}
+3
View File
@@ -0,0 +1,3 @@
package odin_printer
#panic("The printer package has been deprecated. Please look at https://github.com/DanielGavin/ols")
-922
View File
@@ -1,922 +0,0 @@
package odin_printer
import "core:odin/ast"
import "core:odin/tokenizer"
import "core:strings"
import "core:fmt"
import "core:mem"
Type_Enum :: enum {Line_Comment, Value_Decl, Switch_Stmt, Struct, Assign, Call, Enum, If, For, Proc_Lit}
Line_Type :: bit_set[Type_Enum]
/*
Represents an unwrapped line
*/
Line :: struct {
format_tokens: [dynamic]Format_Token,
finalized: bool,
used: bool,
depth: int,
types: Line_Type, //for performance, so you don't have to verify what types are in it by going through the tokens - might give problems when adding linebreaking
}
/*
Represents a singular token in a unwrapped line
*/
Format_Token :: struct {
kind: tokenizer.Token_Kind,
text: string,
type: Type_Enum,
spaces_before: int,
parameter_count: int,
}
Printer :: struct {
string_builder: strings.Builder,
config: Config,
depth: int, //the identation depth
comments: [dynamic]^ast.Comment_Group,
latest_comment_index: int,
allocator: mem.Allocator,
file: ^ast.File,
source_position: tokenizer.Pos,
last_source_position: tokenizer.Pos,
lines: [dynamic]Line, //need to look into a better data structure, one that can handle inserting lines rather than appending
skip_semicolon: bool,
current_line: ^Line,
current_line_index: int,
last_line_index: int,
last_token: ^Format_Token,
merge_next_token: bool,
space_next_token: bool,
debug: bool,
}
Config :: struct {
spaces: int, //Spaces per indentation
newline_limit: int, //The limit of newlines between statements and declarations.
tabs: bool, //Enable or disable tabs
convert_do: bool, //Convert all do statements to brace blocks
semicolons: bool, //Enable semicolons
split_multiple_stmts: bool,
align_switch: bool,
brace_style: Brace_Style,
align_assignments: bool,
align_structs: bool,
align_style: Alignment_Style,
align_enums: bool,
align_length_break: int,
indent_cases: bool,
newline_style: Newline_Style,
}
Brace_Style :: enum {
_1TBS,
Allman,
Stroustrup,
K_And_R,
}
Block_Type :: enum {
None,
If_Stmt,
Proc,
Generic,
Comp_Lit,
Switch_Stmt,
}
Alignment_Style :: enum {
Align_On_Type_And_Equals,
Align_On_Colon_And_Equals,
}
Newline_Style :: enum {
CRLF,
LF,
}
default_style := Config {
spaces = 4,
newline_limit = 2,
convert_do = false,
semicolons = false,
tabs = true,
brace_style = ._1TBS,
split_multiple_stmts = true,
align_assignments = true,
align_style = .Align_On_Type_And_Equals,
indent_cases = false,
align_switch = true,
align_structs = true,
align_enums = true,
newline_style = .CRLF,
align_length_break = 9,
}
make_printer :: proc(config: Config, allocator := context.allocator) -> Printer {
return {
config = config,
allocator = allocator,
debug = false,
}
}
print :: proc(p: ^Printer, file: ^ast.File) -> string {
p.comments = file.comments
if len(file.decls) > 0 {
p.lines = make([dynamic]Line, 0, (file.decls[len(file.decls) - 1].end.line - file.decls[0].pos.line) * 2, context.temp_allocator)
}
set_source_position(p, file.pkg_token.pos)
p.last_source_position.line = 1
set_line(p, 0)
push_generic_token(p, .Package, 0)
push_ident_token(p, file.pkg_name, 1)
for decl in file.decls {
visit_decl(p, cast(^ast.Decl)decl)
}
if len(p.comments) > 0 {
infinite := p.comments[len(p.comments) - 1].end
infinite.offset = 9999999
push_comments(p, infinite)
}
fix_lines(p)
builder := strings.builder_make(0, 5 * mem.Megabyte, p.allocator)
last_line := 0
newline: string
if p.config.newline_style == .LF {
newline = "\n"
} else {
newline = "\r\n"
}
for line, line_index in p.lines {
diff_line := line_index - last_line
for i := 0; i < diff_line; i += 1 {
strings.write_string(&builder, newline)
}
if p.config.tabs {
for i := 0; i < line.depth; i += 1 {
strings.write_byte(&builder, '\t')
}
} else {
for i := 0; i < line.depth * p.config.spaces; i += 1 {
strings.write_byte(&builder, ' ')
}
}
if p.debug {
strings.write_string(&builder, fmt.tprintf("line %v: ", line_index))
}
for format_token in line.format_tokens {
for i := 0; i < format_token.spaces_before; i += 1 {
strings.write_byte(&builder, ' ')
}
strings.write_string(&builder, format_token.text)
}
last_line = line_index
}
strings.write_string(&builder, newline)
return strings.to_string(builder)
}
fix_lines :: proc(p: ^Printer) {
align_var_decls(p)
format_generic(p)
align_comments(p) //align them last since they rely on the other alignments
}
format_value_decl :: proc(p: ^Printer, index: int) {
eq_found := false
eq_token: Format_Token
eq_line: int
largest := 0
found_eq: for line, line_index in p.lines[index:] {
for format_token in line.format_tokens {
largest += len(format_token.text) + format_token.spaces_before
if format_token.kind == .Eq {
eq_token = format_token
eq_line = line_index + index
eq_found = true
break found_eq
}
}
}
if !eq_found {
return
}
align_next := false
//check to see if there is a binary operator in the last token(this is guaranteed by the ast visit), otherwise it's not multilined
for line in p.lines[eq_line:] {
if len(line.format_tokens) == 0 {
break
}
if align_next {
line.format_tokens[0].spaces_before = largest + 1
align_next = false
}
kind := find_last_token(line.format_tokens).kind
if tokenizer.Token_Kind.B_Operator_Begin < kind && kind <= tokenizer.Token_Kind.Cmp_Or {
align_next = true
}
if !align_next {
break
}
}
}
find_last_token :: proc(format_tokens: [dynamic]Format_Token) -> Format_Token {
for i := len(format_tokens) - 1; i >= 0; i -= 1 {
if format_tokens[i].kind != .Comment {
return format_tokens[i]
}
}
panic("not possible")
}
format_assignment :: proc(p: ^Printer, index: int) {
}
format_call :: proc(p: ^Printer, line_index: int, format_index: int) {
paren_found := false
paren_token: Format_Token
paren_line: int
paren_token_index: int
largest := 0
found_paren: for line, i in p.lines[line_index:] {
for format_token, j in line.format_tokens {
largest += len(format_token.text) + format_token.spaces_before
if i == 0 && j < format_index {
continue
}
if format_token.kind == .Open_Paren && format_token.type == .Call {
paren_token = format_token
paren_line = line_index + i
paren_found = true
paren_token_index = j
break found_paren
}
}
}
if !paren_found {
panic("Should not be possible")
}
paren_count := 1
done := false
for line in p.lines[paren_line:] {
if len(line.format_tokens) == 0 {
continue
}
for format_token, i in line.format_tokens {
if format_token.kind == .Comment {
continue
}
if line_index == 0 && i <= paren_token_index {
continue
}
if format_token.kind == .Open_Paren {
paren_count += 1
} else if format_token.kind == .Close_Paren {
paren_count -= 1
}
if paren_count == 0 {
done = true
}
}
if line_index != 0 {
line.format_tokens[0].spaces_before = largest
}
if done {
return
}
}
}
format_keyword_to_brace :: proc(p: ^Printer, line_index: int, format_index: int, keyword: tokenizer.Token_Kind) {
keyword_found := false
keyword_token: Format_Token
keyword_line: int
largest := 0
brace_count := 0
done := false
found_keyword: for line, i in p.lines[line_index:] {
for format_token in line.format_tokens {
largest += len(format_token.text) + format_token.spaces_before
if format_token.kind == keyword {
keyword_token = format_token
keyword_line = line_index + i
keyword_found = true
break found_keyword
}
}
}
if !keyword_found {
panic("Should not be possible")
}
for line, line_idx in p.lines[keyword_line:] {
if len(line.format_tokens) == 0 {
continue
}
for format_token, i in line.format_tokens {
if format_token.kind == .Comment {
break
} else if format_token.kind == .Undef {
return
}
if line_idx == 0 && i <= format_index {
continue
}
if format_token.kind == .Open_Brace {
brace_count += 1
} else if format_token.kind == .Close_Brace {
brace_count -= 1
}
if brace_count == 1 {
done = true
}
}
if line_idx != 0 {
line.format_tokens[0].spaces_before = largest + 1
}
if done {
return
}
}
}
format_generic :: proc(p: ^Printer) {
next_struct_line := 0
for line, line_index in p.lines {
if len(line.format_tokens) <= 0 {
continue
}
for format_token, token_index in line.format_tokens {
#partial switch format_token.kind {
case .For, .If, .When, .Switch:
format_keyword_to_brace(p, line_index, token_index, format_token.kind)
case .Proc:
if format_token.type == .Proc_Lit {
format_keyword_to_brace(p, line_index, token_index, format_token.kind)
}
case:
if format_token.type == .Call {
format_call(p, line_index, token_index)
}
}
}
if .Switch_Stmt in line.types && p.config.align_switch {
align_switch_stmt(p, line_index)
}
if .Enum in line.types && p.config.align_enums {
align_enum(p, line_index)
}
if .Struct in line.types && p.config.align_structs && next_struct_line <= 0 {
next_struct_line = align_struct(p, line_index)
}
if .Value_Decl in line.types {
format_value_decl(p, line_index)
}
if .Assign in line.types {
format_assignment(p, line_index)
}
next_struct_line -= 1
}
}
align_var_decls :: proc(p: ^Printer) {
current_line: int
current_typed: bool
current_not_mutable: bool
largest_lhs := 0
largest_rhs := 0
TokenAndLength :: struct {
format_token: ^Format_Token,
length: int,
}
colon_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
type_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
equal_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
for line, line_index in p.lines {
//It is only possible to align value decls that are one one line, otherwise just ignore them
if .Value_Decl not_in line.types {
continue
}
typed := true
not_mutable := false
continue_flag := false
for i := 0; i < len(line.format_tokens); i += 1 {
if line.format_tokens[i].kind == .Colon && line.format_tokens[min(i + 1, len(line.format_tokens) - 1)].kind == .Eq {
typed = false
}
if line.format_tokens[i].kind == .Colon && line.format_tokens[min(i + 1, len(line.format_tokens) - 1)].kind == .Colon {
not_mutable = true
}
if line.format_tokens[i].kind == .Union ||
line.format_tokens[i].kind == .Enum ||
line.format_tokens[i].kind == .Struct ||
line.format_tokens[i].kind == .For ||
line.format_tokens[i].kind == .If ||
line.format_tokens[i].kind == .Comment {
continue_flag = true
}
//enforced undef is always on the last line, if it exists
if line.format_tokens[i].kind == .Proc && line.format_tokens[len(line.format_tokens)-1].kind != .Undef {
continue_flag = true
}
}
if continue_flag {
continue
}
if line_index != current_line + 1 || typed != current_typed || not_mutable != current_not_mutable {
if p.config.align_style == .Align_On_Colon_And_Equals || !current_typed || current_not_mutable {
for colon_token in colon_tokens {
colon_token.format_token.spaces_before = largest_lhs - colon_token.length + 1
}
} else if p.config.align_style == .Align_On_Type_And_Equals {
for type_token in type_tokens {
type_token.format_token.spaces_before = largest_lhs - type_token.length + 1
}
}
if current_typed {
for equal_token in equal_tokens {
equal_token.format_token.spaces_before = largest_rhs - equal_token.length + 1
}
} else {
for equal_token in equal_tokens {
equal_token.format_token.spaces_before = 0
}
}
clear(&colon_tokens)
clear(&type_tokens)
clear(&equal_tokens)
largest_rhs = 0
largest_lhs = 0
current_typed = typed
current_not_mutable = not_mutable
}
current_line = line_index
current_token_index := 0
lhs_length := 0
rhs_length := 0
//calcuate the length of lhs of a value decl i.e. `a, b:`
for; current_token_index < len(line.format_tokens); current_token_index += 1 {
lhs_length += len(line.format_tokens[current_token_index].text) + line.format_tokens[current_token_index].spaces_before
if line.format_tokens[current_token_index].kind == .Colon {
append(&colon_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index], length = lhs_length})
if len(line.format_tokens) > current_token_index && line.format_tokens[current_token_index + 1].kind != .Eq {
append(&type_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index + 1], length = lhs_length})
}
current_token_index += 1
largest_lhs = max(largest_lhs, lhs_length)
break
}
}
//calcuate the length of the rhs i.e. `[dynamic]int = 123123`
for; current_token_index < len(line.format_tokens); current_token_index += 1 {
rhs_length += len(line.format_tokens[current_token_index].text) + line.format_tokens[current_token_index].spaces_before
if line.format_tokens[current_token_index].kind == .Eq {
append(&equal_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index], length = rhs_length})
largest_rhs = max(largest_rhs, rhs_length)
break
}
}
}
//repeating myself, move to sub procedure
if p.config.align_style == .Align_On_Colon_And_Equals || !current_typed || current_not_mutable {
for colon_token in colon_tokens {
colon_token.format_token.spaces_before = largest_lhs - colon_token.length + 1
}
} else if p.config.align_style == .Align_On_Type_And_Equals {
for type_token in type_tokens {
type_token.format_token.spaces_before = largest_lhs - type_token.length + 1
}
}
if current_typed {
for equal_token in equal_tokens {
equal_token.format_token.spaces_before = largest_rhs - equal_token.length + 1
}
} else {
for equal_token in equal_tokens {
equal_token.format_token.spaces_before = 0
}
}
}
align_switch_stmt :: proc(p: ^Printer, index: int) {
switch_found := false
brace_token: Format_Token
brace_line: int
found_switch_brace: for line, line_index in p.lines[index:] {
for format_token in line.format_tokens {
if format_token.kind == .Open_Brace && switch_found {
brace_token = format_token
brace_line = line_index + index
break found_switch_brace
} else if format_token.kind == .Open_Brace {
break
} else if format_token.kind == .Switch {
switch_found = true
}
}
}
if !switch_found {
return
}
largest := 0
case_count := 0
TokenAndLength :: struct {
format_token: ^Format_Token,
length: int,
}
format_tokens := make([dynamic]TokenAndLength, 0, brace_token.parameter_count, context.temp_allocator)
//find all the switch cases that are one lined
for line in p.lines[brace_line + 1:] {
case_found := false
colon_found := false
length := 0
for format_token, i in line.format_tokens {
if format_token.kind == .Comment {
break
}
//this will only happen if the case is one lined
if case_found && colon_found {
append(&format_tokens, TokenAndLength {format_token = &line.format_tokens[i], length = length})
largest = max(length, largest)
break
}
if format_token.kind == .Case {
case_found = true
case_count += 1
} else if format_token.kind == .Colon {
colon_found = true
}
length += len(format_token.text) + format_token.spaces_before
}
if case_count >= brace_token.parameter_count {
break
}
}
for token in format_tokens {
token.format_token.spaces_before = largest - token.length + 1
}
}
align_enum :: proc(p: ^Printer, index: int) {
enum_found := false
brace_token: Format_Token
brace_line: int
found_enum_brace: for line, line_index in p.lines[index:] {
for format_token in line.format_tokens {
if format_token.kind == .Open_Brace && enum_found {
brace_token = format_token
brace_line = line_index + index
break found_enum_brace
} else if format_token.kind == .Open_Brace {
break
} else if format_token.kind == .Enum {
enum_found = true
}
}
}
if !enum_found {
return
}
largest := 0
comma_count := 0
TokenAndLength :: struct {
format_token: ^Format_Token,
length: int,
}
format_tokens := make([dynamic]TokenAndLength, 0, brace_token.parameter_count, context.temp_allocator)
for line in p.lines[brace_line + 1:] {
length := 0
for format_token, i in line.format_tokens {
if format_token.kind == .Comment {
break
}
if format_token.kind == .Eq {
append(&format_tokens, TokenAndLength {format_token = &line.format_tokens[i], length = length})
largest = max(length, largest)
break
} else if format_token.kind == .Comma {
comma_count += 1
}
length += len(format_token.text) + format_token.spaces_before
}
if comma_count >= brace_token.parameter_count {
break
}
}
for token in format_tokens {
token.format_token.spaces_before = largest - token.length + 1
}
}
align_struct :: proc(p: ^Printer, index: int) -> int {
struct_found := false
brace_token: Format_Token
brace_line: int
found_struct_brace: for line, line_index in p.lines[index:] {
for format_token in line.format_tokens {
if format_token.kind == .Open_Brace && struct_found {
brace_token = format_token
brace_line = line_index + index
break found_struct_brace
} else if format_token.kind == .Open_Brace {
break
} else if format_token.kind == .Struct {
struct_found = true
}
}
}
if !struct_found {
return 0
}
largest := 0
colon_count := 0
nested := false
seen_brace := false
TokenAndLength :: struct {
format_token: ^Format_Token,
length: int,
}
format_tokens := make([]TokenAndLength, brace_token.parameter_count, context.temp_allocator)
if brace_token.parameter_count == 0 {
return 0
}
end_line_index := 0
for line, line_index in p.lines[brace_line + 1:] {
length := 0
for format_token, i in line.format_tokens {
//give up on nested structs
if format_token.kind == .Comment {
break
} else if format_token.kind == .Open_Paren {
break
} else if format_token.kind == .Open_Brace {
seen_brace = true
} else if format_token.kind == .Close_Brace {
seen_brace = false
} else if seen_brace {
continue
}
if format_token.kind == .Colon {
format_tokens[colon_count] = {format_token = &line.format_tokens[i + 1], length = length}
if format_tokens[colon_count].format_token.kind == .Struct {
nested = true
}
colon_count += 1
largest = max(length, largest)
}
length += len(format_token.text) + format_token.spaces_before
}
if nested {
end_line_index = line_index + brace_line + 1
}
if colon_count >= brace_token.parameter_count {
break
}
}
//give up aligning nested, it never looks good
if nested {
for line, line_index in p.lines[end_line_index:] {
for format_token in line.format_tokens {
if format_token.kind == .Close_Brace {
return end_line_index + line_index - index
}
}
}
}
for token in format_tokens {
token.format_token.spaces_before = largest - token.length + 1
}
return 0
}
align_comments :: proc(p: ^Printer) {
Comment_Align_Info :: struct {
length: int,
begin: int,
end: int,
depth: int,
}
comment_infos := make([dynamic]Comment_Align_Info, 0, context.temp_allocator)
current_info: Comment_Align_Info
for line, line_index in p.lines {
if len(line.format_tokens) <= 0 {
continue
}
if .Line_Comment in line.types {
if current_info.end + 1 != line_index || current_info.depth != line.depth ||
(current_info.begin == current_info.end && current_info.length == 0) {
if (current_info.begin != 0 && current_info.end != 0) || current_info.length > 0 {
append(&comment_infos, current_info)
}
current_info.begin = line_index
current_info.end = line_index
current_info.depth = line.depth
current_info.length = 0
}
length := 0
for format_token in line.format_tokens {
if format_token.kind == .Comment {
current_info.length = max(current_info.length, length)
current_info.end = line_index
}
length += format_token.spaces_before + len(format_token.text)
}
}
}
if (current_info.begin != 0 && current_info.end != 0) || current_info.length > 0 {
append(&comment_infos, current_info)
}
for info in comment_infos {
if info.begin == info.end || info.length == 0 {
continue
}
for i := info.begin; i <= info.end; i += 1 {
l := p.lines[i]
length := 0
for format_token in l.format_tokens {
if format_token.kind == .Comment {
if len(l.format_tokens) == 1 {
l.format_tokens[i].spaces_before = info.length + 1
} else {
l.format_tokens[i].spaces_before = info.length - length + 1
}
}
length += format_token.spaces_before + len(format_token.text)
}
}
}
}
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -222,7 +222,7 @@ prefix_length :: proc(a, b: $T/[]$E) -> (n: int) where intrinsics.type_is_compar
}
@(require_results)
has_prefix :: proc(array: $T/[]$E, needle: E) -> bool where intrinsics.type_is_comparable(E) {
has_prefix :: proc(array: $T/[]$E, needle: T) -> bool where intrinsics.type_is_comparable(E) {
n := len(needle)
if len(array) >= n {
return equal(array[:n], needle)
@@ -232,7 +232,7 @@ has_prefix :: proc(array: $T/[]$E, needle: E) -> bool where intrinsics.type_is_c
@(require_results)
has_suffix :: proc(array: $T/[]$E, needle: E) -> bool where intrinsics.type_is_comparable(E) {
has_suffix :: proc(array: $T/[]$E, needle: T) -> bool where intrinsics.type_is_comparable(E) {
array := array
m, n := len(array), len(needle)
if m >= n {
+6 -12
View File
@@ -17,9 +17,6 @@ when ODIN_OS == .Darwin {
}
os_sync_wait_on_address_flag :: enum u32 {
// This flag should be used as a default flag when no other flags listed below are required.
NONE,
// This flag should be used when synchronizing among multiple processes by
// placing the @addr passed to os_sync_wait_on_address and its variants
// in a shared memory region.
@@ -31,15 +28,12 @@ os_sync_wait_on_address_flag :: enum u32 {
// This flag should not be used when synchronizing among multiple threads of
// a single process. It allows the kernel to perform performance optimizations
// as the @addr is local to the calling process.
SHARED,
SHARED = 0,
}
os_sync_wait_on_address_flags :: bit_set[os_sync_wait_on_address_flag; u32]
os_sync_wait_on_address_flags :: distinct bit_set[os_sync_wait_on_address_flag; u32]
os_sync_wake_by_address_flag :: enum u32 {
// This flag should be used as a default flag when no other flags listed below are required.
NONE,
// This flag should be used when synchronizing among multiple processes by
// placing the @addr passed to os_sync_wake_by_address_any and its variants
// in a shared memory region.
@@ -51,10 +45,10 @@ os_sync_wake_by_address_flag :: enum u32 {
// This flag should not be used when synchronizing among multiple threads of
// a single process. It allows the kernel to perform performance optimizations
// as the @addr is local the calling process.
SHARED,
SHARED = 0,
}
os_sync_wake_by_address_flags :: bit_set[os_sync_wake_by_address_flag; u32]
os_sync_wake_by_address_flags :: distinct bit_set[os_sync_wake_by_address_flag; u32]
os_clockid :: enum u32 {
MACH_ABSOLUTE_TIME = 32,
@@ -283,7 +277,7 @@ foreign system {
// and the shared memory specification
// (See os_sync_wake_by_address_flags_t).
// ENOENT : No waiter(s) found waiting on the @addr.
os_sync_wake_by_address_any :: proc(addr: rawptr, size: uint, flags: os_sync_wait_on_address_flags) -> i32 ---
os_sync_wake_by_address_any :: proc(addr: rawptr, size: uint, flags: os_sync_wake_by_address_flags) -> i32 ---
// This function is a variant of os_sync_wake_by_address_any that wakes up all waiters
// blocked in os_sync_wait_on_address or its variants.
@@ -305,5 +299,5 @@ foreign system {
// In the event of an error, returns -1 with errno set to indicate the error.
//
// This function returns same error codes as returned by os_sync_wait_on_address.
os_sync_wake_by_address_all :: proc(addr: rawptr, size: uint, flags: os_sync_wait_on_address_flags) -> i32 ---
os_sync_wake_by_address_all :: proc(addr: rawptr, size: uint, flags: os_sync_wake_by_address_flags) -> i32 ---
}
+30 -13
View File
@@ -183,16 +183,17 @@ undo_check :: proc(s: ^State) {
}
// insert text into the edit state - deletes the current selection
input_text :: proc(s: ^State, text: string) {
input_text :: proc(s: ^State, text: string) -> int {
if len(text) == 0 {
return
return 0
}
if has_selection(s) {
selection_delete(s)
}
insert(s, s.selection[0], text)
offset := s.selection[0] + len(text)
n := insert(s, s.selection[0], text)
offset := s.selection[0] + n
s.selection = {offset, offset}
return n
}
// insert slice of runes into the edit state - deletes the current selection
@@ -206,8 +207,11 @@ input_runes :: proc(s: ^State, text: []rune) {
offset := s.selection[0]
for r in text {
b, w := utf8.encode_rune(r)
insert(s, offset, string(b[:w]))
offset += w
n := insert(s, offset, string(b[:w]))
offset += n
if n != w {
break
}
}
s.selection = {offset, offset}
}
@@ -219,17 +223,29 @@ input_rune :: proc(s: ^State, r: rune) {
}
offset := s.selection[0]
b, w := utf8.encode_rune(r)
insert(s, offset, string(b[:w]))
offset += w
n := insert(s, offset, string(b[:w]))
offset += n
s.selection = {offset, offset}
}
// insert a single rune into the edit state - deletes the current selection
insert :: proc(s: ^State, at: int, text: string) {
insert :: proc(s: ^State, at: int, text: string) -> int {
undo_check(s)
if s.builder != nil {
inject_at(&s.builder.buf, at, text)
if ok, _ := inject_at(&s.builder.buf, at, text); !ok {
n := cap(s.builder.buf) - len(s.builder.buf)
assert(n < len(text))
for is_continuation_byte(text[n]) {
n -= 1
}
if ok2, _ := inject_at(&s.builder.buf, at, text[:n]); !ok2 {
n = 0
}
return n
}
return len(text)
}
return 0
}
// remove the wanted range withing, usually the selection within byte indices
@@ -263,11 +279,12 @@ selection_delete :: proc(s: ^State) {
s.selection = {lo, lo}
}
is_continuation_byte :: proc(b: byte) -> bool {
return b >= 0x80 && b < 0xc0
}
// translates the caret position
translate_position :: proc(s: ^State, t: Translation) -> int {
is_continuation_byte :: proc(b: byte) -> bool {
return b >= 0x80 && b < 0xc0
}
is_space :: proc(b: byte) -> bool {
return b == ' ' || b == '\t' || b == '\n'
}
+36 -17
View File
@@ -60,10 +60,6 @@ parse_mo_from_bytes :: proc(data: []byte, options := DEFAULT_PARSE_OPTIONS, plur
translation.pluralize = pluralizer
strings.intern_init(&translation.intern, allocator, allocator)
// Gettext MO files only have one section.
translation.k_v[""] = {}
section := &translation.k_v[""]
for n := u32(0); n < count; n += 1 {
/*
Grab string's original length and offset.
@@ -83,37 +79,60 @@ parse_mo_from_bytes :: proc(data: []byte, options := DEFAULT_PARSE_OPTIONS, plur
max_offset := int(max(o_offset + o_length + 1, t_offset + t_length + 1))
if len(data) < max_offset { return translation, .Premature_EOF }
key := data[o_offset:][:o_length]
val := data[t_offset:][:t_length]
key_data := data[o_offset:][:o_length]
val_data := data[t_offset:][:t_length]
/*
Could be a pluralized string.
*/
zero := []byte{0}
keys := bytes.split(key_data, zero); defer delete(keys)
vals := bytes.split(val_data, zero); defer delete(vals)
keys := bytes.split(key, zero)
vals := bytes.split(val, zero)
if len(keys) != len(vals) || max(len(keys), len(vals)) > MAX_PLURALS {
if (len(keys) != 1 && len(keys) != 2) || len(vals) > MAX_PLURALS {
return translation, .MO_File_Incorrect_Plural_Count
}
for k in keys {
interned_key, _ := strings.intern_get(&translation.intern, string(k))
section_name := ""
key := string(k)
interned_vals := make([]string, len(keys))
// Scan for <context>EOT<key>
for ch, i in k {
if ch == 0x04 {
section_name = string(k[:i])
key = string(k[i+1:])
break
}
}
// If we merge sections, then all entries end in the "" context.
if options.merge_sections {
section_name = ""
}
section_name, _ = strings.intern_get(&translation.intern, section_name)
if section_name not_in translation.k_v {
translation.k_v[section_name] = {}
}
section := &translation.k_v[section_name]
interned_key, _ := strings.intern_get(&translation.intern, string(key))
// Duplicate key should not be allowed.
if interned_key in section {
return translation, .Duplicate_Key
}
interned_vals := make([]string, len(vals))
last_val: string
i := 0
for v in vals {
for v, i in vals {
interned_vals[i], _ = strings.intern_get(&translation.intern, string(v))
last_val = interned_vals[i]
i += 1
}
section[interned_key] = interned_vals
}
delete(vals)
delete(keys)
}
return
}
+113
View File
@@ -0,0 +1,113 @@
package time
// Parsing ISO 8601 date/time strings into time.Time.
import dt "core:time/datetime"
// Parses an ISO 8601 string and returns Time in UTC, with any UTC offset applied to it.
// Only 4-digit years are accepted.
// Optional pointer to boolean `is_leap` will return `true` if the moment was a leap second.
// Leap seconds are smeared into 23:59:59.
iso8601_to_time_utc :: proc(iso_datetime: string, is_leap: ^bool = nil) -> (res: Time, consumed: int) {
offset: int
res, offset, consumed = iso8601_to_time_and_offset(iso_datetime, is_leap)
res._nsec += (i64(-offset) * i64(Minute))
return res, consumed
}
// Parses an ISO 8601 string and returns Time and a UTC offset in minutes.
// e.g. 1985-04-12T23:20:50.52Z
// Note: Only 4-digit years are accepted.
// Optional pointer to boolean `is_leap` will return `true` if the moment was a leap second.
// Leap seconds are smeared into 23:59:59.
iso8601_to_time_and_offset :: proc(iso_datetime: string, is_leap: ^bool = nil) -> (res: Time, utc_offset: int, consumed: int) {
moment, offset, leap_second, count := iso8601_to_components(iso_datetime)
if count == 0 {
return
}
if is_leap != nil {
is_leap^ = leap_second
}
if _res, ok := datetime_to_time(moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, moment.nano); !ok {
return {}, 0, 0
} else {
return _res, offset, count
}
}
// Parses an ISO 8601 string and returns Time and a UTC offset in minutes.
// e.g. 1985-04-12T23:20:50.52Z
// Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given
iso8601_to_components :: proc(iso_datetime: string) -> (res: dt.DateTime, utc_offset: int, is_leap: bool, consumed: int) {
moment, offset, count, leap_second, ok := _iso8601_to_components(iso_datetime)
if !ok {
return
}
return moment, offset, leap_second, count
}
// Parses an ISO 8601 string and returns datetime.DateTime.
// Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given
@(private)
_iso8601_to_components :: proc(iso_datetime: string) -> (res: dt.DateTime, utc_offset: int, consumed: int, is_leap: bool, ok: bool) {
// A compliant date is at minimum 20 characters long, e.g. YYYY-MM-DDThh:mm:ssZ
(len(iso_datetime) >= 20) or_return
// Scan and eat YYYY-MM-DD[Tt], then scan and eat HH:MM:SS, leave separator
year := scan_digits(iso_datetime[0:], "-", 4) or_return
month := scan_digits(iso_datetime[5:], "-", 2) or_return
day := scan_digits(iso_datetime[8:], "Tt ", 2) or_return
hour := scan_digits(iso_datetime[11:], ":", 2) or_return
minute := scan_digits(iso_datetime[14:], ":", 2) or_return
second := scan_digits(iso_datetime[17:], "", 2) or_return
nanos := 0
count := 19
// Scan fractional seconds
if iso_datetime[count] == '.' {
count += 1 // consume '.'
multiplier := 100_000_000
for digit in iso_datetime[count:] {
if multiplier >= 1 && int(digit) >= '0' && int(digit) <= '9' {
nanos += int(digit - '0') * multiplier
multiplier /= 10
count += 1
} else {
break
}
}
}
// Leap second handling
if minute == 59 && second == 60 {
second = 59
is_leap = true
}
err: dt.Error
if res, err = dt.components_to_datetime(year, month, day, hour, minute, second, nanos); err != .None {
return {}, 0, 0, false, false
}
if len(iso_datetime[count:]) == 0 {
return res, utc_offset, count, is_leap, true
}
// Scan UTC offset
switch iso_datetime[count] {
case 'Z', 'z':
utc_offset = 0
count += 1
case '+', '-':
(len(iso_datetime[count:]) >= 6) or_return
offset_hour := scan_digits(iso_datetime[count+1:], ":", 2) or_return
offset_minute := scan_digits(iso_datetime[count+4:], "", 2) or_return
utc_offset = 60 * offset_hour + offset_minute
utc_offset *= -1 if iso_datetime[count] == '-' else 1
count += 6
}
return res, utc_offset, count, is_leap, true
}
+7 -7
View File
@@ -57,12 +57,12 @@ _rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_o
(len(rfc_datetime) >= 20) or_return
// Scan and eat YYYY-MM-DD[Tt], then scan and eat HH:MM:SS, leave separator
year := scan_digits(rfc_datetime[0:], "-", 4) or_return
month := scan_digits(rfc_datetime[5:], "-", 2) or_return
day := scan_digits(rfc_datetime[8:], "Tt", 2) or_return
hour := scan_digits(rfc_datetime[11:], ":", 2) or_return
minute := scan_digits(rfc_datetime[14:], ":", 2) or_return
second := scan_digits(rfc_datetime[17:], "", 2) or_return
year := scan_digits(rfc_datetime[0:], "-", 4) or_return
month := scan_digits(rfc_datetime[5:], "-", 2) or_return
day := scan_digits(rfc_datetime[8:], "Tt ", 2) or_return
hour := scan_digits(rfc_datetime[11:], ":", 2) or_return
minute := scan_digits(rfc_datetime[14:], ":", 2) or_return
second := scan_digits(rfc_datetime[17:], "", 2) or_return
nanos := 0
count := 19
@@ -87,7 +87,7 @@ _rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_o
// Scan UTC offset
switch rfc_datetime[count] {
case 'Z':
case 'Z', 'z':
utc_offset = 0
count += 1
case '+', '-':
-2
View File
@@ -2,8 +2,6 @@
//+build wasi
package time
import wasi "core:sys/wasm/wasi"
_IS_SUPPORTED :: false
_now :: proc "contextless" () -> Time {
+2 -4
View File
@@ -21,6 +21,7 @@ import queue "core:container/queue"
import small_array "core:container/small_array"
import lru "core:container/lru"
import list "core:container/intrusive/list"
import rbtree "core:container/rbtree"
import topological_sort "core:container/topological_sort"
import crypto "core:crypto"
@@ -91,9 +92,7 @@ import virtual "core:mem/virtual"
import ast "core:odin/ast"
import doc_format "core:odin/doc-format"
import odin_format "core:odin/format"
import odin_parser "core:odin/parser"
import odin_printer "core:odin/printer"
import odin_tokenizer "core:odin/tokenizer"
import spall "core:prof/spall"
@@ -147,6 +146,7 @@ _ :: queue
_ :: small_array
_ :: lru
_ :: list
_ :: rbtree
_ :: topological_sort
_ :: crypto
_ :: crypto_hash
@@ -207,9 +207,7 @@ _ :: mem
_ :: virtual
_ :: ast
_ :: doc_format
_ :: odin_format
_ :: odin_parser
_ :: odin_printer
_ :: odin_tokenizer
_ :: os
_ :: spall
+1 -1
View File
@@ -251,7 +251,7 @@ gb_internal void report_ram_info() {
int result = sysinfo(&info);
if (result == 0x0) {
gb_printf("%lu MiB\n", info.totalram * info.mem_unit / gb_megabytes(1));
gb_printf("%lu MiB\n", (unsigned long)(info.totalram * info.mem_unit / gb_megabytes(1)));
} else {
gb_printf("Unknown.\n");
}
+11 -9
View File
@@ -978,7 +978,7 @@ gb_global TargetMetrics target_linux_arm32 = {
TargetOs_linux,
TargetArch_arm32,
4, 4, 4, 8,
str_lit("arm-linux-gnu"),
str_lit("arm-unknown-linux-gnueabihf"),
};
gb_global TargetMetrics target_darwin_amd64 = {
@@ -1906,6 +1906,16 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta
#else
metrics = &target_linux_amd64;
#endif
#elif defined(GB_CPU_ARM)
#if defined(GB_SYSTEM_WINDOWS)
#error "Build Error: Unsupported architecture"
#elif defined(GB_SYSTEM_OSX)
#error "Build Error: Unsupported architecture"
#elif defined(GB_SYSTEM_FREEBSD)
#error "Build Error: Unsupported architecture"
#else
metrics = &target_linux_arm32;
#endif
#else
#if defined(GB_SYSTEM_WINDOWS)
metrics = &target_windows_i386;
@@ -2052,14 +2062,6 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta
if (bc->metrics.os == TargetOs_freestanding) {
bc->ODIN_DEFAULT_TO_NIL_ALLOCATOR = !bc->ODIN_DEFAULT_TO_PANIC_ALLOCATOR;
} else if (is_arch_wasm()) {
if (bc->metrics.os == TargetOs_js || bc->metrics.os == TargetOs_wasi) {
// TODO(bill): Should these even have a default "heap-like" allocator?
}
if (!bc->ODIN_DEFAULT_TO_NIL_ALLOCATOR && !bc->ODIN_DEFAULT_TO_PANIC_ALLOCATOR) {
bc->ODIN_DEFAULT_TO_PANIC_ALLOCATOR = true;
}
}
}
+35 -1
View File
@@ -1092,7 +1092,13 @@ gb_internal bool cache_load_file_directive(CheckerContext *c, Ast *call, String
BlockingMutex *ignore_mutex = nullptr;
bool ok = determine_path_from_string(ignore_mutex, call, base_dir, original_string, &path);
gb_unused(ok);
if (!ok) {
if (err_on_not_found) {
error(ce->proc, "Failed to `#%.*s` file: %.*s; invalid file or cannot be found", LIT(builtin_name), LIT(original_string));
}
call->state_flags |= StateFlag_DirectiveWasFalse;
return false;
}
}
MUTEX_GUARD(&c->info->load_file_mutex);
@@ -5235,6 +5241,34 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As
operand->type = t_untyped_bool;
break;
case BuiltinProc_type_is_matrix_row_major:
case BuiltinProc_type_is_matrix_column_major:
{
Operand op = {};
Type *bt = check_type(c, ce->args[0]);
Type *type = base_type(bt);
if (type == nullptr || type == t_invalid) {
error(ce->args[0], "Expected a type for '%.*s'", LIT(builtin_name));
return false;
}
if (type->kind != Type_Matrix) {
gbString s = type_to_string(bt);
error(ce->args[0], "Expected a matrix type for '%.*s', got '%s'", LIT(builtin_name), s);
gb_string_free(s);
return false;
}
if (id == BuiltinProc_type_is_matrix_row_major) {
operand->value = exact_value_bool(bt->Matrix.is_row_major == true);
} else {
operand->value = exact_value_bool(bt->Matrix.is_row_major == false);
}
operand->mode = Addressing_Constant;
operand->type = t_untyped_bool;
break;
}
case BuiltinProc_type_has_field:
{
Operand op = {};
+55 -6
View File
@@ -1179,15 +1179,59 @@ gb_internal void check_assignment(CheckerContext *c, Operand *operand, Type *typ
LIT(context_name));
check_assignment_error_suggestion(c, operand, type);
Type *src = base_type(operand->type);
Type *dst = base_type(type);
if (context_name == "procedure argument") {
Type *src = base_type(operand->type);
Type *dst = base_type(type);
if (is_type_slice(src) && are_types_identical(src->Slice.elem, dst)) {
gbString a = expr_to_string(operand->expr);
error_line("\tSuggestion: Did you mean to pass the slice into the variadic parameter with ..%s?\n\n", a);
gb_string_free(a);
}
}
if (src->kind == dst->kind && src->kind == Type_Proc) {
Type *x = src;
Type *y = dst;
bool same_inputs = are_types_identical_internal(x->Proc.params, y->Proc.params, false);
bool same_outputs = are_types_identical_internal(x->Proc.results, y->Proc.results, false);
if (same_inputs && same_outputs &&
x->Proc.calling_convention != y->Proc.calling_convention) {
gbString s_expected = type_to_string(y);
gbString s_got = type_to_string(x);
error_line("\tNote: The calling conventions differ between the procedure signature types\n");
error_line("\t Expected \"%s\", got \"%s\"\n",
proc_calling_convention_strings[y->Proc.calling_convention],
proc_calling_convention_strings[x->Proc.calling_convention]);
error_line("\t Expected: %s\n", s_expected);
error_line("\t Got: %s\n", s_got);
gb_string_free(s_got);
gb_string_free(s_expected);
} else if (same_inputs && !same_outputs) {
gbString s_expected = type_to_string(y->Proc.results);
gbString s_got = type_to_string(x->Proc.results);
error_line("\tNote: The return types differ between the procedure signature types\n");
error_line("\t Expected: %s\n", s_expected);
error_line("\t Got: %s\n", s_got);
gb_string_free(s_got);
gb_string_free(s_expected);
} else if (!same_inputs && same_outputs) {
gbString s_expected = type_to_string(y->Proc.params);
gbString s_got = type_to_string(x->Proc.params);
error_line("\tNote: The input parameter types differ between the procedure signature types\n");
error_line("\t Expected: %s\n", s_expected);
error_line("\t Got: %s\n", s_got);
gb_string_free(s_got);
gb_string_free(s_expected);
} else {
gbString s_expected = type_to_string(y);
gbString s_got = type_to_string(x);
error_line("\tNote: The signature type do not match whatsoever\n");
error_line("\t Expected: %s\n", s_expected);
error_line("\t Got: %s\n", s_got);
gb_string_free(s_got);
gb_string_free(s_expected);
}
}
}
break;
}
@@ -1761,7 +1805,7 @@ gb_internal Entity *check_ident(CheckerContext *c, Operand *o, Ast *n, Type *nam
case Entity_ImportName:
if (!allow_import_name) {
error(n, "Use of import '%.*s' not in selector", LIT(name));
error(n, "Use of import name '%.*s' not in the form of 'x.y'", LIT(name));
}
return e;
case Entity_LibraryName:
@@ -7757,13 +7801,18 @@ gb_internal bool check_set_index_data(Operand *o, Type *t, bool indirection, i64
return true;
case Type_Matrix:
*max_count = t->Matrix.column_count;
if (indirection) {
o->mode = Addressing_Variable;
} else if (o->mode != Addressing_Variable) {
o->mode = Addressing_Value;
}
o->type = alloc_type_array(t->Matrix.elem, t->Matrix.row_count);
if (t->Matrix.is_row_major) {
*max_count = t->Matrix.row_count;
o->type = alloc_type_array(t->Matrix.elem, t->Matrix.column_count);
} else {
*max_count = t->Matrix.column_count;
o->type = alloc_type_array(t->Matrix.elem, t->Matrix.row_count);
}
return true;
case Type_Slice:
@@ -10159,7 +10208,7 @@ gb_internal ExprKind check_index_expr(CheckerContext *c, Operand *o, Ast *node,
o->mode = Addressing_Invalid;
o->expr = node;
return kind;
} else if (ok) {
} else if (ok && !is_type_matrix(t)) {
ExactValue value = type_and_value_of_expr(ie->expr).value;
o->mode = Addressing_Constant;
bool success = false;
+7 -2
View File
@@ -565,7 +565,11 @@ gb_internal Type *check_assignment_variable(CheckerContext *ctx, Operand *lhs, O
} else {
error(lhs->expr, "Cannot assign to '%s' which is a procedure parameter", str);
}
error_line("\tSuggestion: Did you mean to pass '%.*s' by pointer?\n", LIT(e->token.string));
if (is_type_pointer(e->type)) {
error_line("\tSuggestion: Did you mean to shadow it? '%.*s := %.*s'?\n", LIT(e->token.string), LIT(e->token.string));
} else {
error_line("\tSuggestion: Did you mean to pass '%.*s' by pointer?\n", LIT(e->token.string));
}
show_error_on_line(e->token.pos, token_pos_end(e->token));
} else {
ERROR_BLOCK();
@@ -1663,6 +1667,7 @@ gb_internal void check_range_stmt(CheckerContext *ctx, Ast *node, u32 mod_flags)
}
}
}
bool is_ptr = type_deref(operand.type);
Type *t = base_type(type_deref(operand.type));
switch (t->kind) {
@@ -1707,7 +1712,7 @@ gb_internal void check_range_stmt(CheckerContext *ctx, Ast *node, u32 mod_flags)
break;
case Type_Array:
is_possibly_addressable = operand.mode == Addressing_Variable;
is_possibly_addressable = operand.mode == Addressing_Variable || is_ptr;
array_add(&vals, t->Array.elem);
array_add(&vals, t_int);
break;
+1 -1
View File
@@ -1595,7 +1595,7 @@ gb_internal bool is_expr_from_a_parameter(CheckerContext *ctx, Ast *expr) {
return is_expr_from_a_parameter(ctx, lhs);
} else if (expr->kind == Ast_Ident) {
Operand x= {};
Entity *e = check_ident(ctx, &x, expr, nullptr, nullptr, false);
Entity *e = check_ident(ctx, &x, expr, nullptr, nullptr, true);
if (e->flags & EntityFlag_Param) {
return true;
}
+4
View File
@@ -2645,6 +2645,10 @@ gb_internal void generate_minimum_dependency_set(Checker *c, Entity *start) {
str_lit("memmove"),
);
FORCE_ADD_RUNTIME_ENTITIES(build_context.metrics.arch == TargetArch_arm32,
str_lit("aeabi_d2h")
);
FORCE_ADD_RUNTIME_ENTITIES(is_arch_wasm() && !build_context.tilde_backend,
// // Extended data type internal procedures
// str_lit("umodti3"),
+6
View File
@@ -256,6 +256,9 @@ BuiltinProc__type_simple_boolean_begin,
BuiltinProc__type_simple_boolean_end,
BuiltinProc_type_is_matrix_row_major,
BuiltinProc_type_is_matrix_column_major,
BuiltinProc_type_has_field,
BuiltinProc_type_field_type,
@@ -567,6 +570,9 @@ gb_global BuiltinProc builtin_procs[BuiltinProc_COUNT] = {
{STR_LIT("type_has_nil"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics},
{STR_LIT(""), 0, false, Expr_Stmt, BuiltinProcPkg_intrinsics},
{STR_LIT("type_is_matrix_row_major"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics},
{STR_LIT("type_is_matrix_column_major"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics},
{STR_LIT("type_has_field"), 2, false, Expr_Expr, BuiltinProcPkg_intrinsics},
{STR_LIT("type_field_type"), 2, false, Expr_Expr, BuiltinProcPkg_intrinsics},
+4 -3
View File
@@ -6261,11 +6261,12 @@ gb_no_inline isize gb_snprintf_va(char *text, isize max_len, char const *fmt, va
#elif defined(__aarch64__)
gb_inline u64 gb_rdtsc(void) {
int64_t virtual_timer_value;
asm volatile("mrs %0, cntvct_el0" : "=r"(virtual_timer_value));
return virtual_timer_value;
asm volatile("mrs %0, cntvct_el0" : "=r"(virtual_timer_value));
return virtual_timer_value;
}
#else
#error "gb_rdtsc not supported"
#warning "gb_rdtsc not supported"
gb_inline u64 gb_rdtsc(void) { return 0; }
#endif
#if defined(GB_SYSTEM_WINDOWS)
+5 -4
View File
@@ -2384,9 +2384,10 @@ gb_internal lbValue lb_build_builtin_proc(lbProcedure *p, Ast *expr, TypeAndValu
lbValue ptr0 = lb_emit_conv(p, lb_build_expr(p, ce->args[0]), t_uintptr);
lbValue ptr1 = lb_emit_conv(p, lb_build_expr(p, ce->args[1]), t_uintptr);
ptr0 = lb_emit_conv(p, ptr0, t_int);
ptr1 = lb_emit_conv(p, ptr1, t_int);
lbValue diff = lb_emit_arith(p, Token_Sub, ptr0, ptr1, t_uintptr);
diff = lb_emit_conv(p, diff, t_int);
lbValue diff = lb_emit_arith(p, Token_Sub, ptr0, ptr1, t_int);
return lb_emit_arith(p, Token_Quo, diff, lb_const_int(p->module, t_int, type_size_of(elem)), t_int);
}
@@ -2903,7 +2904,6 @@ gb_internal lbValue lb_build_builtin_proc(lbProcedure *p, Ast *expr, TypeAndValu
break;
case TargetArch_arm32:
{
// TODO(bill): Check this is correct
GB_ASSERT(arg_count <= 7);
char asm_string[] = "svc #0";
@@ -2911,13 +2911,14 @@ gb_internal lbValue lb_build_builtin_proc(lbProcedure *p, Ast *expr, TypeAndValu
for (unsigned i = 0; i < arg_count; i++) {
constraints = gb_string_appendc(constraints, ",{");
static char const *regs[] = {
"r8",
"r7",
"r0",
"r1",
"r2",
"r3",
"r4",
"r5",
"r6",
};
constraints = gb_string_appendc(constraints, regs[i]);
constraints = gb_string_appendc(constraints, "}");
+2 -2
View File
@@ -2223,8 +2223,8 @@ gb_internal LLVMAtomicOrdering llvm_atomic_ordering_from_odin(ExactValue const &
GB_ASSERT(value.kind == ExactValue_Integer);
i64 v = exact_value_to_i64(value);
switch (v) {
case OdinAtomicMemoryOrder_relaxed: return LLVMAtomicOrderingUnordered;
case OdinAtomicMemoryOrder_consume: return LLVMAtomicOrderingMonotonic;
case OdinAtomicMemoryOrder_relaxed: return LLVMAtomicOrderingMonotonic;
case OdinAtomicMemoryOrder_consume: return LLVMAtomicOrderingAcquire;
case OdinAtomicMemoryOrder_acquire: return LLVMAtomicOrderingAcquire;
case OdinAtomicMemoryOrder_release: return LLVMAtomicOrderingRelease;
case OdinAtomicMemoryOrder_acq_rel: return LLVMAtomicOrderingAcquireRelease;
+8 -2
View File
@@ -3499,8 +3499,14 @@ gb_internal Array<Ast *> parse_ident_list(AstFile *f, bool allow_poly_names) {
gb_internal Ast *parse_type(AstFile *f) {
Ast *type = parse_type_or_ident(f);
if (type == nullptr) {
Token token = advance_token(f);
syntax_error(token, "Expected a type");
Token prev_token = f->curr_token;
Token token = {};
if (f->curr_token.kind == Token_OpenBrace) {
token = f->curr_token;
} else {
token = advance_token(f);
}
syntax_error(token, "Expected a type, got '%.*s'", LIT(prev_token.string));
return ast_bad_expr(f, token, f->curr_token);
} else if (type->kind == Ast_ParenExpr &&
unparen_expr(type) == nullptr) {
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: 0.1.0\n"
"PO-Revision-Date: 2024-04-13 11:13+0200\n"
"Last-Translator: Someone <someone@somewhere.com>\n"
"Language-Team: English\n"
"Language: en_IE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgctxt "Context"
msgid "Message1"
msgstr "This is message 1 with Context"
msgid "Message1"
msgstr "This is message 1 without Context"
Binary file not shown.
+17
View File
@@ -0,0 +1,17 @@
msgid ""
msgstr ""
"Project-Id-Version: 0.1.0\n"
"PO-Revision-Date: 2024-04-13 11:13+0200\n"
"Last-Translator: Someone <someone@somewhere.com>\n"
"Language-Team: English\n"
"Language: it_IT\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2);\n"
msgid "Message1"
msgid_plural "Message1/plural"
msgstr[0] "This is message 1"
msgstr[1] "This is message 1 - plural A"
msgstr[2] "This is message 1 - plural B"
+40 -45
View File
@@ -3,30 +3,15 @@ set COMMON=-no-bounds-check -vet -strict-style
set COLLECTION=-collection:tests=..
set PATH_TO_ODIN==..\..\odin
python3 download_assets.py
echo ---
echo Running core:image tests
echo ---
%PATH_TO_ODIN% run image %COMMON% -out:test_core_image.exe || exit /b
echo ---
echo Running core:compress tests
echo ---
%PATH_TO_ODIN% run compress %COMMON% -out:test_core_compress.exe || exit /b
echo ---
echo Running core:strings tests
echo Running core:container tests
echo ---
%PATH_TO_ODIN% run strings %COMMON% -out:test_core_strings.exe || exit /b
echo ---
echo Running core:hash tests
echo ---
%PATH_TO_ODIN% run hash %COMMON% -o:size -out:test_core_hash.exe || exit /b
echo ---
echo Running core:odin tests
echo ---
%PATH_TO_ODIN% run odin %COMMON% -o:size -out:test_core_odin.exe || exit /b
%PATH_TO_ODIN% run container %COMMON% %COLLECTION% -out:test_core_container.exe || exit /b
echo ---
echo Running core:crypto tests
@@ -45,9 +30,19 @@ rem %PATH_TO_ODIN% run encoding/hxa %COMMON% %COLLECTION% -out:test_hxa.exe |
%PATH_TO_ODIN% run encoding/base64 %COMMON% -out:test_base64.exe || exit /b
echo ---
echo Running core:math/noise tests
echo Running core:fmt tests
echo ---
%PATH_TO_ODIN% run math/noise %COMMON% -out:test_noise.exe || exit /b
%PATH_TO_ODIN% run fmt %COMMON% %COLLECTION% -out:test_core_fmt.exe || exit /b
echo ---
echo Running core:hash tests
echo ---
%PATH_TO_ODIN% run hash %COMMON% -o:size -out:test_core_hash.exe || exit /b
echo ---
echo Running core:image tests
echo ---
%PATH_TO_ODIN% run image %COMMON% -out:test_core_image.exe || exit /b
echo ---
echo Running core:math tests
@@ -59,6 +54,21 @@ echo Running core:math/linalg/glsl tests
echo ---
%PATH_TO_ODIN% run math/linalg/glsl %COMMON% %COLLECTION% -out:test_linalg_glsl.exe || exit /b
echo ---
echo Running core:math/noise tests
echo ---
%PATH_TO_ODIN% run math/noise %COMMON% -out:test_noise.exe || exit /b
echo ---
echo Running core:net
echo ---
%PATH_TO_ODIN% run net %COMMON% -out:test_core_net.exe || exit /b
echo ---
echo Running core:odin tests
echo ---
%PATH_TO_ODIN% run odin %COMMON% -o:size -out:test_core_odin.exe || exit /b
echo ---
echo Running core:path/filepath tests
echo ---
@@ -69,47 +79,32 @@ echo Running core:reflect tests
echo ---
%PATH_TO_ODIN% run reflect %COMMON% %COLLECTION% -out:test_core_reflect.exe || exit /b
echo ---
echo Running core:runtime tests
echo ---
%PATH_TO_ODIN% run runtime %COMMON% %COLLECTION% -out:test_core_runtime.exe || exit /b
echo ---
echo Running core:slice tests
echo ---
%PATH_TO_ODIN% run slice %COMMON% -out:test_core_slice.exe || exit /b
echo ---
echo Running core:strings tests
echo ---
%PATH_TO_ODIN% run strings %COMMON% -out:test_core_strings.exe || exit /b
echo ---
echo Running core:text/i18n tests
echo ---
%PATH_TO_ODIN% run text\i18n %COMMON% -out:test_core_i18n.exe || exit /b
echo ---
echo Running core:net
echo ---
%PATH_TO_ODIN% run net %COMMON% -out:test_core_net.exe || exit /b
echo ---
echo Running core:slice tests
echo ---
%PATH_TO_ODIN% run slice %COMMON% -out:test_core_slice.exe || exit /b
echo ---
echo Running core:container tests
echo ---
%PATH_TO_ODIN% run container %COMMON% %COLLECTION% -out:test_core_container.exe || exit /b
echo ---
echo Running core:thread tests
echo ---
%PATH_TO_ODIN% run thread %COMMON% %COLLECTION% -out:test_core_thread.exe || exit /b
echo ---
echo Running core:runtime tests
echo ---
%PATH_TO_ODIN% run runtime %COMMON% %COLLECTION% -out:test_core_runtime.exe || exit /b
echo ---
echo Running core:time tests
echo ---
%PATH_TO_ODIN% run time %COMMON% %COLLECTION% -out:test_core_time.exe || exit /b
echo ---
echo Running core:fmt tests
echo ---
%PATH_TO_ODIN% run fmt %COMMON% %COLLECTION% -out:test_core_fmt.exe || exit /b
%PATH_TO_ODIN% run time %COMMON% %COLLECTION% -out:test_core_time.exe || exit /b
+11 -8
View File
@@ -4,12 +4,12 @@ import "core:container/avl"
import "core:math/rand"
import "core:slice"
import "core:testing"
import "core:fmt"
import tc "tests:common"
@(test)
test_avl :: proc(t: ^testing.T) {
tc.log(t, "Testing avl")
tc.log(t, fmt.tprintf("Testing avl, using random seed %v, add -define:RANDOM_SEED=%v to reuse it.", random_seed, random_seed))
// Initialization.
tree: avl.Tree(int)
@@ -21,11 +21,14 @@ test_avl :: proc(t: ^testing.T) {
iter := avl.iterator(&tree, avl.Direction.Forward)
tc.expect(t, avl.iterator_get(&iter) == nil, "empty/iterator: first node should be nil")
r: rand.Rand
rand.init(&r, random_seed)
// Test insertion.
NR_INSERTS :: 32 + 1 // Ensure at least 1 collision.
inserted_map := make(map[int]^avl.Node(int))
for i := 0; i < NR_INSERTS; i += 1 {
v := int(rand.uint32() & 0x1f)
v := int(rand.uint32(&r) & 0x1f)
existing_node, in_map := inserted_map[v]
n, ok, _ := avl.find_or_insert(&tree, v)
@@ -38,7 +41,7 @@ test_avl :: proc(t: ^testing.T) {
}
nrEntries := len(inserted_map)
tc.expect(t, avl.len(&tree) == nrEntries, "insert: len after")
tree_validate(t, &tree)
validate_avl(t, &tree)
// Ensure that all entries can be found.
for k, v in inserted_map {
@@ -74,7 +77,7 @@ test_avl :: proc(t: ^testing.T) {
tc.expect(t, visited == nrEntries, "iterator/backward: visited")
// Test removal.
rand.shuffle(inserted_values[:])
rand.shuffle(inserted_values[:], &r)
for v, i in inserted_values {
node := avl.find(&tree, v)
tc.expect(t, node != nil, "remove: find (pre)")
@@ -82,7 +85,7 @@ test_avl :: proc(t: ^testing.T) {
ok := avl.remove(&tree, v)
tc.expect(t, ok, "remove: succeeds")
tc.expect(t, nrEntries - (i + 1) == avl.len(&tree), "remove: len (post)")
tree_validate(t, &tree)
validate_avl(t, &tree)
tc.expect(t, nil == avl.find(&tree, v), "remove: find (post")
}
@@ -114,7 +117,7 @@ test_avl :: proc(t: ^testing.T) {
tc.expect(t, ok == (avl.len(&tree) > 0), "iterator/remove: next should return false")
tc.expect(t, node == avl.first(&tree), "iterator/remove: next should return first")
tree_validate(t, &tree)
validate_avl(t, &tree)
}
tc.expect(t, avl.len(&tree) == nrEntries - 1, "iterator/remove: len should drop by 1")
@@ -123,7 +126,7 @@ test_avl :: proc(t: ^testing.T) {
}
@(private)
tree_validate :: proc(t: ^testing.T, tree: ^avl.Tree($Value)) {
validate_avl :: proc(t: ^testing.T, tree: ^avl.Tree($Value)) {
tree_check_invariants(t, tree, tree._root, nil)
}
@@ -20,7 +20,7 @@ main :: proc() {
t := testing.T{}
test_avl(&t)
test_rbtree(&t)
test_small_array(&t)
tc.report(&t)
}
+244
View File
@@ -0,0 +1,244 @@
package test_core_container
import rb "core:container/rbtree"
import "core:math/rand"
import "core:testing"
import "core:fmt"
import "base:intrinsics"
import "core:mem"
import "core:slice"
import tc "tests:common"
RANDOM_SEED :: #config(RANDOM_SEED, 0)
random_seed := u64(intrinsics.read_cycle_counter()) when RANDOM_SEED == 0 else u64(RANDOM_SEED)
test_rbtree_integer :: proc(t: ^testing.T, $Key: typeid, $Value: typeid) {
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
context.allocator = mem.tracking_allocator(&track)
r: rand.Rand
rand.init(&r, random_seed)
tc.log(t, fmt.tprintf("Testing Red-Black Tree($Key=%v,$Value=%v), using random seed %v, add -define:RANDOM_SEED=%v to reuse it.", type_info_of(Key), type_info_of(Value), random_seed, random_seed))
tree: rb.Tree(Key, Value)
rb.init(&tree)
tc.expect(t, rb.len(&tree) == 0, "empty: len should be 0")
tc.expect(t, rb.first(&tree) == nil, "empty: first should be nil")
tc.expect(t, rb.last(&tree) == nil, "empty: last should be nil")
iter := rb.iterator(&tree, .Forward)
tc.expect(t, rb.iterator_get(&iter) == nil, "empty/iterator: first node should be nil")
// Test insertion.
NR_INSERTS :: 32 + 1 // Ensure at least 1 collision.
inserted_map := make(map[Key]^rb.Node(Key, Value))
min_key := max(Key)
max_key := min(Key)
for i := 0; i < NR_INSERTS; i += 1 {
k := Key(rand.uint32(&r)) & 0x1f
min_key = min(min_key, k); max_key = max(max_key, k)
v := Value(rand.uint32(&r))
existing_node, in_map := inserted_map[k]
n, inserted, _ := rb.find_or_insert(&tree, k, v)
tc.expect(t, in_map != inserted, "insert: inserted should match inverse of map lookup")
if inserted {
inserted_map[k] = n
} else {
tc.expect(t, existing_node == n, "insert: expecting existing node")
}
}
entry_count := len(inserted_map)
tc.expect(t, rb.len(&tree) == entry_count, "insert: len after")
validate_rbtree(t, &tree)
first := rb.first(&tree)
last := rb.last(&tree)
tc.expect(t, first != nil && first.key == min_key, fmt.tprintf("insert: first should be present with key %v", min_key))
tc.expect(t, last != nil && last.key == max_key, fmt.tprintf("insert: last should be present with key %v", max_key))
// Ensure that all entries can be found.
for k, v in inserted_map {
tc.expect(t, v == rb.find(&tree, k), "Find(): Node")
tc.expect(t, k == v.key, "Find(): Node key")
}
// Test the forward/backward iterators.
inserted_keys: [dynamic]Key
for k in inserted_map {
append(&inserted_keys, k)
}
slice.sort(inserted_keys[:])
iter = rb.iterator(&tree, rb.Direction.Forward)
visited: int
for node in rb.iterator_next(&iter) {
k, idx := node.key, visited
tc.expect(t, inserted_keys[idx] == k, "iterator/forward: key")
tc.expect(t, node == rb.iterator_get(&iter), "iterator/forward: get")
visited += 1
}
tc.expect(t, visited == entry_count, "iterator/forward: visited")
slice.reverse(inserted_keys[:])
iter = rb.iterator(&tree, rb.Direction.Backward)
visited = 0
for node in rb.iterator_next(&iter) {
k, idx := node.key, visited
tc.expect(t, inserted_keys[idx] == k, "iterator/backward: key")
visited += 1
}
tc.expect(t, visited == entry_count, "iterator/backward: visited")
// Test removal (and on_remove callback)
rand.shuffle(inserted_keys[:], &r)
callback_count := entry_count
tree.user_data = &callback_count
tree.on_remove = proc(key: Key, value: Value, user_data: rawptr) {
(^int)(user_data)^ -= 1
}
for k, i in inserted_keys {
node := rb.find(&tree, k)
tc.expect(t, node != nil, "remove: find (pre)")
ok := rb.remove(&tree, k)
tc.expect(t, ok, "remove: succeeds")
tc.expect(t, entry_count - (i + 1) == rb.len(&tree), "remove: len (post)")
validate_rbtree(t, &tree)
tc.expect(t, nil == rb.find(&tree, k), "remove: find (post")
}
tc.expect(t, rb.len(&tree) == 0, "remove: len should be 0")
tc.expect(t, callback_count == 0, fmt.tprintf("remove: on_remove should've been called %v times, it was %v", entry_count, callback_count))
tc.expect(t, rb.first(&tree) == nil, "remove: first should be nil")
tc.expect(t, rb.last(&tree) == nil, "remove: last should be nil")
// Refill the tree.
for k in inserted_keys {
rb.find_or_insert(&tree, k, 42)
}
// Test that removing the node doesn't break the iterator.
callback_count = entry_count
iter = rb.iterator(&tree, rb.Direction.Forward)
if node := rb.iterator_get(&iter); node != nil {
k := node.key
ok := rb.iterator_remove(&iter)
tc.expect(t, ok, "iterator/remove: success")
ok = rb.iterator_remove(&iter)
tc.expect(t, !ok, "iterator/remove: redundant removes should fail")
tc.expect(t, rb.find(&tree, k) == nil, "iterator/remove: node should be gone")
tc.expect(t, rb.iterator_get(&iter) == nil, "iterator/remove: get should return nil")
// Ensure that iterator_next still works.
node, ok = rb.iterator_next(&iter)
tc.expect(t, ok == (rb.len(&tree) > 0), "iterator/remove: next should return false")
tc.expect(t, node == rb.first(&tree), "iterator/remove: next should return first")
validate_rbtree(t, &tree)
}
tc.expect(t, rb.len(&tree) == entry_count - 1, "iterator/remove: len should drop by 1")
rb.destroy(&tree)
tc.expect(t, rb.len(&tree) == 0, "destroy: len should be 0")
tc.expect(t, callback_count == 0, fmt.tprintf("remove: on_remove should've been called %v times, it was %v", entry_count, callback_count))
// print_tree_node(tree._root)
delete(inserted_map)
delete(inserted_keys)
tc.expect(t, len(track.allocation_map) == 0, fmt.tprintf("Expected 0 leaks, have %v", len(track.allocation_map)))
tc.expect(t, len(track.bad_free_array) == 0, fmt.tprintf("Expected 0 bad frees, have %v", len(track.bad_free_array)))
return
}
@(test)
test_rbtree :: proc(t: ^testing.T) {
test_rbtree_integer(t, u16, u16)
}
print_tree_node :: proc(n: ^$N/rb.Node($Key, $Value), indent := 0) {
if n == nil {
fmt.println("<empty tree>")
return
}
if n.right != nil {
print_tree_node(n.right, indent + 1)
}
for _ in 0..<indent {
fmt.printf("\t")
}
if n.color == .Black {
fmt.printfln("%v", n.key)
} else {
fmt.printfln("<%v>", n.key)
}
if n.left != nil {
print_tree_node(n.left, indent + 1)
}
}
validate_rbtree :: proc(t: ^testing.T, tree: ^$T/rb.Tree($Key, $Value)) {
verify_rbtree_propery_1(t, tree._root)
verify_rbtree_propery_2(t, tree._root)
/* Property 3 is implicit */
verify_rbtree_propery_4(t, tree._root)
verify_rbtree_propery_5(t, tree._root)
}
verify_rbtree_propery_1 :: proc(t: ^testing.T, n: ^$N/rb.Node($Key, $Value)) {
tc.expect(t, rb.node_color(n) == .Black || rb.node_color(n) == .Red, "Property #1: Each node is either red or black.")
if n == nil {
return
}
verify_rbtree_propery_1(t, n._left)
verify_rbtree_propery_1(t, n._right)
}
verify_rbtree_propery_2 :: proc(t: ^testing.T, root: ^$N/rb.Node($Key, $Value)) {
tc.expect(t, rb.node_color(root) == .Black, "Property #2: Root node should be black.")
}
verify_rbtree_propery_4 :: proc(t: ^testing.T, n: ^$N/rb.Node($Key, $Value)) {
if rb.node_color(n) == .Red {
// A red node's left, right and parent should be black
all_black := rb.node_color(n._left) == .Black && rb.node_color(n._right) == .Black && rb.node_color(n._parent) == .Black
tc.expect(t, all_black, "Property #3: Red node's children + parent must be black.")
}
if n == nil {
return
}
verify_rbtree_propery_4(t, n._left)
verify_rbtree_propery_4(t, n._right)
}
verify_rbtree_propery_5 :: proc(t: ^testing.T, root: ^$N/rb.Node($Key, $Value)) {
black_count_path := -1
verify_rbtree_propery_5_helper(t, root, 0, &black_count_path)
}
verify_rbtree_propery_5_helper :: proc(t: ^testing.T, n: ^$N/rb.Node($Key, $Value), black_count: int, path_black_count: ^int) {
black_count := black_count
if rb.node_color(n) == .Black {
black_count += 1
}
if n == nil {
if path_black_count^ == -1 {
path_black_count^ = black_count
} else {
tc.expect(t, black_count == path_black_count^, "Property #5: Paths from a node to its leaves contain same black count.")
}
return
}
verify_rbtree_propery_5_helper(t, n._left, black_count, path_black_count)
verify_rbtree_propery_5_helper(t, n._right, black_count, path_black_count)
}
// Properties 4 and 5 together guarantee that no path in the tree is more than about twice as long as any other path,
// which guarantees that it has O(log n) height.
-12
View File
@@ -3,9 +3,7 @@ package test_core_odin_parser
import "core:fmt"
import "core:odin/ast"
import "core:odin/parser"
import "core:odin/printer"
import "core:os"
import "core:strings"
import "core:testing"
@@ -81,14 +79,4 @@ Foo :: bit_field uint {
p := parser.default_parser()
ok := parser.parse_file(&p, &file)
expect(t, ok == true, "bad parse")
cfg := printer.default_style
cfg.newline_style = .LF
print := printer.make_printer(cfg)
out := printer.print(&print, &file)
tsrc := strings.trim_space(file.src)
tout := strings.trim_space(out)
expect(t, tsrc == tout, fmt.tprintf("\n%s\n!=\n%s", tsrc, tout))
}
+82 -25
View File
@@ -38,44 +38,98 @@ Test :: struct {
Test_Suite :: struct {
file: string,
loader: proc(string, i18n.Parse_Options, proc(int) -> int, mem.Allocator) -> (^i18n.Translation, i18n.Error),
plural: proc(int) -> int,
err: i18n.Error,
options: i18n.Parse_Options,
tests: []Test,
}
// Custom pluralizer for plur.mo
plur_mo_pluralizer :: proc(n: int) -> (slot: int) {
switch {
case n == 1: return 0
case n != 0 && n % 1_000_000 == 0: return 1
case: return 2
}
}
TESTS := []Test_Suite{
{
file = "assets/I18N/plur.mo",
loader = i18n.parse_mo_file,
plural = plur_mo_pluralizer,
tests = {
// These are in the catalog.
{"", "Message1", "This is message 1", 1},
{"", "Message1", "This is message 1 - plural A", 1_000_000},
{"", "Message1", "This is message 1 - plural B", 42},
{"", "Message1/plural", "This is message 1", 1},
{"", "Message1/plural", "This is message 1 - plural A", 1_000_000},
{"", "Message1/plural", "This is message 1 - plural B", 42},
// This isn't in the catalog, so should ruturn the key.
{"", "Come visit us on Discord!", "Come visit us on Discord!", 1},
},
},
{
file = "assets/I18N/mixed_context.mo",
loader = i18n.parse_mo_file,
plural = nil,
tests = {
// These are in the catalog.
{"", "Message1", "This is message 1 without Context", 1},
{"Context", "Message1", "This is message 1 with Context", 1},
// This isn't in the catalog, so should ruturn the key.
{"", "Come visit us on Discord!", "Come visit us on Discord!", 1},
},
},
{
file = "assets/I18N/mixed_context.mo",
loader = i18n.parse_mo_file,
plural = nil,
// Message1 exists twice, once within Context, which has been merged into ""
err = .Duplicate_Key,
options = {merge_sections = true},
},
{
file = "assets/I18N/nl_NL.mo",
loader = i18n.parse_mo_file,
plural = nil, // Default pluralizer
tests = {
// These are in the catalog.
{ "", "There are 69,105 leaves here.", "Er zijn hier 69.105 bladeren.", 1 },
{ "", "Hellope, World!", "Hallo, Wereld!", 1 },
{ "", "There is %d leaf.\n", "Er is %d blad.\n", 1 },
{ "", "There are %d leaves.\n", "Er is %d blad.\n", 1 },
{ "", "There is %d leaf.\n", "Er zijn %d bladeren.\n", 42 },
{ "", "There are %d leaves.\n", "Er zijn %d bladeren.\n", 42 },
{"", "There are 69,105 leaves here.", "Er zijn hier 69.105 bladeren.", 1},
{"", "Hellope, World!", "Hallo, Wereld!", 1},
{"", "There is %d leaf.\n", "Er is %d blad.\n", 1},
{"", "There are %d leaves.\n", "Er is %d blad.\n", 1},
{"", "There is %d leaf.\n", "Er zijn %d bladeren.\n", 42},
{"", "There are %d leaves.\n", "Er zijn %d bladeren.\n", 42},
// This isn't in the catalog, so should ruturn the key.
{ "", "Come visit us on Discord!", "Come visit us on Discord!", 1 },
{"", "Come visit us on Discord!", "Come visit us on Discord!", 1},
},
},
// QT Linguist with default loader options.
{
file = "assets/I18N/nl_NL-qt-ts.ts",
loader = i18n.parse_qt_linguist_file,
plural = nil, // Default pluralizer
tests = {
// These are in the catalog.
{ "Page", "Text for translation", "Tekst om te vertalen", 1},
{ "Page", "Also text to translate", "Ook tekst om te vertalen", 1},
{ "installscript", "99 bottles of beer on the wall", "99 flessen bier op de muur", 1},
{ "apple_count", "%d apple(s)", "%d appel", 1},
{ "apple_count", "%d apple(s)", "%d appels", 42},
{"Page", "Text for translation", "Tekst om te vertalen", 1},
{"Page", "Also text to translate", "Ook tekst om te vertalen", 1},
{"installscript", "99 bottles of beer on the wall", "99 flessen bier op de muur", 1},
{"apple_count", "%d apple(s)", "%d appel", 1},
{"apple_count", "%d apple(s)", "%d appels", 42},
// These aren't in the catalog, so should ruturn the key.
{ "", "Come visit us on Discord!", "Come visit us on Discord!", 1 },
{ "Fake_Section", "Come visit us on Discord!", "Come visit us on Discord!", 1 },
{"", "Come visit us on Discord!", "Come visit us on Discord!", 1},
{"Fake_Section", "Come visit us on Discord!", "Come visit us on Discord!", 1},
},
},
@@ -83,21 +137,22 @@ TESTS := []Test_Suite{
{
file = "assets/I18N/nl_NL-qt-ts.ts",
loader = i18n.parse_qt_linguist_file,
plural = nil, // Default pluralizer
options = {merge_sections = true},
tests = {
// All of them are now in section "", lookup with original section should return the key.
{ "", "Text for translation", "Tekst om te vertalen", 1},
{ "", "Also text to translate", "Ook tekst om te vertalen", 1},
{ "", "99 bottles of beer on the wall", "99 flessen bier op de muur", 1},
{ "", "%d apple(s)", "%d appel", 1},
{ "", "%d apple(s)", "%d appels", 42},
{"", "Text for translation", "Tekst om te vertalen", 1},
{"", "Also text to translate", "Ook tekst om te vertalen", 1},
{"", "99 bottles of beer on the wall", "99 flessen bier op de muur", 1},
{"", "%d apple(s)", "%d appel", 1},
{"", "%d apple(s)", "%d appels", 42},
// All of them are now in section "", lookup with original section should return the key.
{ "Page", "Text for translation", "Text for translation", 1},
{ "Page", "Also text to translate", "Also text to translate", 1},
{ "installscript", "99 bottles of beer on the wall", "99 bottles of beer on the wall", 1},
{ "apple_count", "%d apple(s)", "%d apple(s)", 1},
{ "apple_count", "%d apple(s)", "%d apple(s)", 42},
{"Page", "Text for translation", "Text for translation", 1},
{"Page", "Also text to translate", "Also text to translate", 1},
{"installscript", "99 bottles of beer on the wall", "99 bottles of beer on the wall", 1},
{"apple_count", "%d apple(s)", "%d apple(s)", 1},
{"apple_count", "%d apple(s)", "%d apple(s)", 42},
},
},
@@ -105,6 +160,7 @@ TESTS := []Test_Suite{
{
file = "assets/I18N/duplicate-key.ts",
loader = i18n.parse_qt_linguist_file,
plural = nil, // Default pluralizer
options = {merge_sections = true},
err = .Duplicate_Key,
},
@@ -113,6 +169,7 @@ TESTS := []Test_Suite{
{
file = "assets/I18N/duplicate-key.ts",
loader = i18n.parse_qt_linguist_file,
plural = nil, // Default pluralizer
},
}
@@ -122,7 +179,7 @@ tests :: proc(t: ^testing.T) {
err: i18n.Error
for suite in TESTS {
cat, err = suite.loader(suite.file, suite.options, nil, context.allocator)
cat, err = suite.loader(suite.file, suite.options, suite.plural, context.allocator)
msg := fmt.tprintf("Expected loading %v to return %v, got %v", suite.file, suite.err, err)
expect(t, err == suite.err, msg)
+70 -3
View File
@@ -42,6 +42,7 @@ main :: proc() {
test_ordinal_date_roundtrip(&t)
test_component_to_time_roundtrip(&t)
test_parse_rfc3339_string(&t)
test_parse_iso8601_string(&t)
for _, leak in track.allocation_map {
expect(&t, false, fmt.tprintf("%v leaked %m\n", leak.location, leak.size))
@@ -91,7 +92,46 @@ RFC3339_Test :: struct{
// These are based on RFC 3339's examples, see https://www.rfc-editor.org/rfc/rfc3339#page-10
rfc3339_tests :: []RFC3339_Test{
// This represents 20 minutes and 50.52 seconds after the 23rd hour of April 12th, 1985 in UTC.
{"1985-04-12T23:20:50.52Z", {482196050520000000}, true, 0, 23, false},
{"1985-04-12 23:20:50.52Z", {482196050520000000}, true, 0, 23, false},
// Same, but lowercase z
{"1985-04-12 23:20:50.52z", {482196050520000000}, true, 0, 23, false},
// This represents 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time).
// Note that this is equivalent to 1996-12-20T00:39:57Z in UTC.
{"1996-12-19 16:39:57-08:00", {851013597000000000}, false, -480, 25, false},
{"1996-12-19 16:39:57-08:00", {851042397000000000}, true, 0, 25, false},
{"1996-12-20 00:39:57Z", {851042397000000000}, false, 0, 20, false},
// This represents the leap second inserted at the end of 1990.
// It'll be represented as 1990-12-31 23:59:59 UTC after parsing, and `is_leap` will be set to `true`.
{"1990-12-31 23:59:60Z", {662687999000000000}, true, 0, 20, true},
// This represents the same leap second in Pacific Standard Time, 8 hours behind UTC.
{"1990-12-31 15:59:60-08:00", {662687999000000000}, true, 0, 25, true},
// This represents the same instant of time as noon, January 1, 1937, Netherlands time.
// Standard time in the Netherlands was exactly 19 minutes and 32.13 seconds ahead of UTC by law
// from 1909-05-01 through 1937-06-30. This time zone cannot be represented exactly using the
// HH:MM format, and this timestamp uses the closest representable UTC offset.
{"1937-01-01 12:00:27.87+00:20", {-1041335972130000000}, false, 20, 28, false},
{"1937-01-01 12:00:27.87+00:20", {-1041337172130000000}, true, 0, 28, false},
}
ISO8601_Test :: struct{
iso_8601: string,
datetime: time.Time,
apply_offset: bool,
utc_offset: int,
consumed: int,
is_leap: bool,
}
// These are based on RFC 3339's examples, see https://www.rfc-editor.org/rfc/rfc3339#page-10
iso8601_tests :: []ISO8601_Test{
// This represents 20 minutes and .003362 seconds after the 23rd hour of April 12th, 1985 in UTC.
{"1985-04-12T23:20:50.003362", {482196050003362000}, true, 0, 26, false},
{"1985-04-12t23:20:50.003362", {482196050003362000}, true, 0, 26, false},
{"1985-04-12 23:20:50.003362", {482196050003362000}, true, 0, 26, false},
// This represents 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time).
// Note that this is equivalent to 1996-12-20T00:39:57Z in UTC.
@@ -110,8 +150,8 @@ rfc3339_tests :: []RFC3339_Test{
// Standard time in the Netherlands was exactly 19 minutes and 32.13 seconds ahead of UTC by law
// from 1909-05-01 through 1937-06-30. This time zone cannot be represented exactly using the
// HH:MM format, and this timestamp uses the closest representable UTC offset.
{"1937-01-01T12:00:27.87+00:20", {-1041335972130000000}, false, 20, 28, false},
{"1937-01-01T12:00:27.87+00:20", {-1041337172130000000}, true, 0, 28, false},
{"1937-01-01 12:00:27.87+00:20", {-1041335972130000000}, false, 20, 28, false},
{"1937-01-01 12:00:27.87+00:20", {-1041337172130000000}, true, 0, 28, false},
}
@test
@@ -141,6 +181,33 @@ test_parse_rfc3339_string :: proc(t: ^testing.T) {
}
}
@test
test_parse_iso8601_string :: proc(t: ^testing.T) {
for test in iso8601_tests {
is_leap := false
if test.apply_offset {
res, consumed := time.iso8601_to_time_utc(test.iso_8601, &is_leap)
msg := fmt.tprintf("[apply offet] Parsing failed: %v -> %v (nsec: %v). Expected %v consumed, got %v", test.iso_8601, res, res._nsec, test.consumed, consumed)
expect(t, test.consumed == consumed, msg)
if test.consumed == consumed {
expect(t, test.datetime == res, fmt.tprintf("Time didn't match. Expected %v (%v), got %v (%v)", test.datetime, test.datetime._nsec, res, res._nsec))
expect(t, test.is_leap == is_leap, "Expected a leap second, got none.")
}
} else {
res, offset, consumed := time.iso8601_to_time_and_offset(test.iso_8601)
msg := fmt.tprintf("Parsing failed: %v -> %v (nsec: %v), offset: %v. Expected %v consumed, got %v", test.iso_8601, res, res._nsec, offset, test.consumed, consumed)
expect(t, test.consumed == consumed, msg)
if test.consumed == consumed {
expect(t, test.datetime == res, fmt.tprintf("Time didn't match. Expected %v (%v), got %v (%v)", test.datetime, test.datetime._nsec, res, res._nsec))
expect(t, test.utc_offset == offset, fmt.tprintf("UTC offset didn't match. Expected %v, got %v", test.utc_offset, offset))
expect(t, test.is_leap == is_leap, "Expected a leap second, got none.")
}
}
}
}
MONTH_DAYS := []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
YEAR_START :: 1900
YEAR_END :: 2024
+137 -23
View File
@@ -29,6 +29,7 @@ import "core:sort"
import "core:strings"
import "core:strconv"
import "core:math"
import textedit "core:text/edit"
COMMAND_LIST_SIZE :: #config(MICROUI_COMMAND_LIST_SIZE, 256 * 1024)
ROOT_LIST_SIZE :: #config(MICROUI_ROOT_LIST_SIZE, 32)
@@ -51,6 +52,7 @@ Clip :: enum u32 {
Color_Type :: enum u32 {
TEXT,
SELECTION_BG,
BORDER,
WINDOW_BG,
TITLE_BG,
@@ -111,7 +113,16 @@ Key :: enum u32 {
CTRL,
ALT,
BACKSPACE,
DELETE,
RETURN,
LEFT,
RIGHT,
HOME,
END,
A,
X,
C,
V,
}
Key_Set :: distinct bit_set[Key; u32]
@@ -235,6 +246,8 @@ Context :: struct {
key_down_bits, key_pressed_bits: Key_Set,
_text_store: [MAX_TEXT_STORE]u8,
text_input: strings.Builder, // uses `_text_store` as backing store with nil_allocator.
textbox_state: textedit.State,
textbox_offset: i32,
}
Stack :: struct($T: typeid, $N: int) {
@@ -260,6 +273,7 @@ default_style := Style{
scrollbar_size = 12, thumb_size = 8,
colors = {
.TEXT = {230, 230, 230, 255},
.SELECTION_BG = {90, 90, 90, 255},
.BORDER = {25, 25, 25, 255},
.WINDOW_BG = {50, 50, 50, 255},
.TITLE_BG = {25, 25, 25, 255},
@@ -305,12 +319,16 @@ default_draw_frame :: proc(ctx: ^Context, rect: Rect, colorid: Color_Type) {
}
}
init :: proc(ctx: ^Context) {
init :: proc(ctx: ^Context, set_clipboard: proc(user_data: rawptr, text: string) -> (ok: bool), get_clipboard: proc(user_data: rawptr) -> (text: string, ok: bool), clipboard_user_data: rawptr) {
ctx^ = {} // zero memory
ctx.draw_frame = default_draw_frame
ctx._style = default_style
ctx.style = &ctx._style
ctx.text_input = strings.builder_from_bytes(ctx._text_store[:])
ctx.textbox_state.set_clipboard = set_clipboard
ctx.textbox_state.get_clipboard = get_clipboard
ctx.textbox_state.clipboard_user_data = clipboard_user_data
}
begin :: proc(ctx: ^Context) {
@@ -967,23 +985,95 @@ checkbox :: proc(ctx: ^Context, label: string, state: ^bool) -> (res: Result_Set
textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect, opt := Options{}) -> (res: Result_Set) {
update_control(ctx, id, r, opt | {.HOLD_FOCUS})
font := ctx.style.font
if ctx.focus_id == id {
/* create a builder backed by the user's buffer */
builder := strings.builder_from_bytes(textbuf)
non_zero_resize(&builder.buf, textlen^)
ctx.textbox_state.builder = &builder
if ctx.textbox_state.id != u64(id) {
ctx.textbox_state.id = u64(id)
ctx.textbox_state.selection = {}
}
/* check selection bounds */
if ctx.textbox_state.selection[0] > textlen^ || ctx.textbox_state.selection[1] > textlen^ {
ctx.textbox_state.selection = {}
}
/* handle text input */
n := min(len(textbuf) - textlen^, strings.builder_len(ctx.text_input))
if n > 0 {
copy(textbuf[textlen^:], strings.to_string(ctx.text_input)[:n])
textlen^ += n
if strings.builder_len(ctx.text_input) > 0 {
if textedit.input_text(&ctx.textbox_state, strings.to_string(ctx.text_input)) > 0 {
textlen^ = strings.builder_len(builder)
res += {.CHANGE}
}
}
/* handle ctrl+a */
if .A in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits {
ctx.textbox_state.selection = {textlen^, 0}
}
/* handle ctrl+x */
if .X in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits {
if textedit.cut(&ctx.textbox_state) {
textlen^ = strings.builder_len(builder)
res += {.CHANGE}
}
}
/* handle ctrl+c */
if .C in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits {
textedit.copy(&ctx.textbox_state)
}
/* handle ctrl+v */
if .V in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits {
if textedit.paste(&ctx.textbox_state) {
textlen^ = strings.builder_len(builder)
res += {.CHANGE}
}
}
/* handle left/right */
if .LEFT in ctx.key_pressed_bits {
move: textedit.Translation = .Word_Left if .CTRL in ctx.key_down_bits else .Left
if .SHIFT in ctx.key_down_bits {
textedit.select_to(&ctx.textbox_state, move)
} else {
textedit.move_to(&ctx.textbox_state, move)
}
}
if .RIGHT in ctx.key_pressed_bits {
move: textedit.Translation = .Word_Right if .CTRL in ctx.key_down_bits else .Right
if .SHIFT in ctx.key_down_bits {
textedit.select_to(&ctx.textbox_state, move)
} else {
textedit.move_to(&ctx.textbox_state, move)
}
}
/* handle home/end */
if .HOME in ctx.key_pressed_bits {
if .SHIFT in ctx.key_down_bits {
textedit.select_to(&ctx.textbox_state, .Start)
} else {
textedit.move_to(&ctx.textbox_state, .Start)
}
}
if .END in ctx.key_pressed_bits {
if .SHIFT in ctx.key_down_bits {
textedit.select_to(&ctx.textbox_state, .End)
} else {
textedit.move_to(&ctx.textbox_state, .End)
}
}
/* handle backspace/delete */
if .BACKSPACE in ctx.key_pressed_bits && textlen^ > 0 {
move: textedit.Translation = .Word_Left if .CTRL in ctx.key_down_bits else .Left
textedit.delete_to(&ctx.textbox_state, move)
textlen^ = strings.builder_len(builder)
res += {.CHANGE}
}
/* handle backspace */
if .BACKSPACE in ctx.key_pressed_bits && textlen^ > 0 {
/* skip utf-8 continuation bytes */
for textlen^ > 0 {
textlen^ -= 1
if textbuf[textlen^] & 0xc0 != 0x80 {
break
}
}
if .DELETE in ctx.key_pressed_bits && textlen^ > 0 {
move: textedit.Translation = .Word_Right if .CTRL in ctx.key_down_bits else .Right
textedit.delete_to(&ctx.textbox_state, move)
textlen^ = strings.builder_len(builder)
res += {.CHANGE}
}
/* handle return */
@@ -991,6 +1081,25 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect
set_focus(ctx, 0)
res += {.SUBMIT}
}
/* handle click/drag */
if .LEFT in ctx.mouse_down_bits {
idx := textlen^
for i in 0..<textlen^ {
/* skip continuation bytes */
if textbuf[i] >= 0x80 && textbuf[i] < 0xc0 {
continue
}
if ctx.mouse_pos.x < r.x + ctx.textbox_offset + ctx.text_width(font, string(textbuf[:i])) {
idx = i
break
}
}
ctx.textbox_state.selection[0] = idx
if .LEFT in ctx.mouse_pressed_bits && .SHIFT not_in ctx.key_down_bits {
ctx.textbox_state.selection[1] = idx
}
}
}
textstr := string(textbuf[:textlen^])
@@ -998,16 +1107,21 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect
/* draw */
draw_control_frame(ctx, id, r, .BASE, opt)
if ctx.focus_id == id {
color := ctx.style.colors[.TEXT]
font := ctx.style.font
textw := ctx.text_width(font, textstr)
texth := ctx.text_height(font)
ofx := r.w - ctx.style.padding - textw - 1
textx := r.x + min(ofx, ctx.style.padding)
texty := r.y + (r.h - texth) / 2
text_color := ctx.style.colors[.TEXT]
sel_color := ctx.style.colors[.SELECTION_BG]
textw := ctx.text_width(font, textstr)
texth := ctx.text_height(font)
headx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[0]])
tailx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[1]])
ofmin := max(ctx.style.padding - headx, r.w - textw - ctx.style.padding)
ofmax := min(r.w - headx - ctx.style.padding, ctx.style.padding)
ctx.textbox_offset = clamp(ctx.textbox_offset, ofmin, ofmax)
textx := r.x + ctx.textbox_offset
texty := r.y + (r.h - texth) / 2
push_clip_rect(ctx, r)
draw_text(ctx, font, textstr, Vec2{textx, texty}, color)
draw_rect(ctx, Rect{textx + textw, texty, 1, texth}, color)
draw_rect(ctx, Rect{textx + min(headx, tailx), texty, abs(headx - tailx), texth}, sel_color)
draw_text(ctx, font, textstr, Vec2{textx, texty}, text_color)
draw_rect(ctx, Rect{textx + headx, texty, 1, texth}, text_color)
pop_clip_rect(ctx)
} else {
draw_control_text(ctx, textstr, r, .TEXT, opt)
+45 -13
View File
@@ -13,7 +13,8 @@ function stripNewline(str) {
return str.replace(/\n/, ' ')
}
const STRING_SIZE = 2*4;
const INT_SIZE = 4; // NOTE: set to `8` if the target has 64 bit ints (`wasm64p32` for example).
const STRING_SIZE = 2*INT_SIZE;
class WasmMemoryInterface {
constructor() {
@@ -69,19 +70,34 @@ class WasmMemoryInterface {
const hi = this.mem.getInt32 (addr + 4, true);
return lo + hi*4294967296;
};
loadF32(addr) { return this.mem.getFloat32(addr, true); }
loadF64(addr) { return this.mem.getFloat64(addr, true); }
loadInt(addr) { return this.mem.getInt32 (addr, true); }
loadUint(addr) { return this.mem.getUint32 (addr, true); }
loadPtr(addr) { return this.loadUint(addr); }
loadF32(addr) { return this.mem.getFloat32(addr, true); }
loadF64(addr) { return this.mem.getFloat64(addr, true); }
loadInt(addr) {
if (INT_SIZE == 8) {
return this.loadI64(addr);
} else if (INT_SIZE == 4) {
return this.loadI32(addr);
} else {
throw new Error('Unhandled `INT_SIZE`, expected `4` or `8`');
}
};
loadUint(addr) {
if (INT_SIZE == 8) {
return this.loadU64(addr);
} else if (INT_SIZE == 4) {
return this.loadU32(addr);
} else {
throw new Error('Unhandled `INT_SIZE`, expected `4` or `8`');
}
};
loadPtr(addr) { return this.loadU32(addr); }
loadBytes(ptr, len) {
return new Uint8Array(this.memory.buffer, ptr, len);
return new Uint8Array(this.memory.buffer, ptr, Number(len));
}
loadString(ptr, len) {
const bytes = this.loadBytes(ptr, len);
const bytes = this.loadBytes(ptr, Number(len));
return new TextDecoder().decode(bytes);
}
@@ -99,10 +115,26 @@ class WasmMemoryInterface {
this.mem.setUint32(addr + 0, value, true);
this.mem.setInt32 (addr + 4, Math.floor(value / 4294967296), true);
}
storeF32(addr, value) { this.mem.setFloat32(addr, value, true); }
storeF64(addr, value) { this.mem.setFloat64(addr, value, true); }
storeInt(addr, value) { this.mem.setInt32 (addr, value, true); }
storeUint(addr, value) { this.mem.setUint32 (addr, value, true); }
storeF32(addr, value) { this.mem.setFloat32(addr, value, true); }
storeF64(addr, value) { this.mem.setFloat64(addr, value, true); }
storeInt(addr, value) {
if (INT_SIZE == 8) {
this.storeI64(addr, value);
} else if (INT_SIZE == 4) {
this.storeI32(addr, value);
} else {
throw new Error('Unhandled `INT_SIZE`, expected `4` or `8`');
}
}
storeUint(addr, value) {
if (INT_SIZE == 8) {
this.storeU64(addr, value);
} else if (INT_SIZE == 4) {
this.storeU32(addr, value);
} else {
throw new Error('Unhandled `INT_SIZE`, expected `4` or `8`');
}
}
// Returned length might not be the same as `value.length` if non-ascii strings are given.
storeString(addr, value) {