diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 801bffd87..ffb2077d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/base/intrinsics/intrinsics.odin b/base/intrinsics/intrinsics.odin index 0c4e5d6c3..8873f3bbc 100644 --- a/base/intrinsics/intrinsics.odin +++ b/base/intrinsics/intrinsics.odin @@ -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) --- diff --git a/base/runtime/default_allocators_general.odin b/base/runtime/default_allocators_general.odin index cbaf4d22a..ab4dd1db8 100644 --- a/base/runtime/default_allocators_general.odin +++ b/base/runtime/default_allocators_general.odin @@ -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 diff --git a/base/runtime/default_temporary_allocator.odin b/base/runtime/default_temporary_allocator.odin index c90f0388d..b355ded70 100644 --- a/base/runtime/default_temporary_allocator.odin +++ b/base/runtime/default_temporary_allocator.odin @@ -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 {} diff --git a/base/runtime/internal.odin b/base/runtime/internal.odin index 3e9c524bd..8e1b3d633 100644 --- a/base/runtime/internal.odin +++ b/base/runtime/internal.odin @@ -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) } -} \ No newline at end of file +} diff --git a/base/runtime/os_specific_wasi.odin b/base/runtime/os_specific_wasi.odin index 94fa5fa89..0e229ac7e 100644 --- a/base/runtime/os_specific_wasi.odin +++ b/base/runtime/os_specific_wasi.odin @@ -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) } diff --git a/base/runtime/wasm_allocator.odin b/base/runtime/wasm_allocator.odin new file mode 100644 index 000000000..acfc80b0a --- /dev/null +++ b/base/runtime/wasm_allocator.odin @@ -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.. 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 + +// ..... | ..... | ..... | ..... + +// 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: +// ... + +@(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.. 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 +} diff --git a/build_odin.sh b/build_odin.sh index 7e0d0e585..c27ad119d 100755 --- a/build_odin.sh +++ b/build_odin.sh @@ -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) diff --git a/core/container/avl/avl.odin b/core/container/avl/avl.odin index eecc1b756..582cd87fd 100644 --- a/core/container/avl/avl.odin +++ b/core/container/avl/avl.odin @@ -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) } -} +} \ No newline at end of file diff --git a/core/container/rbtree/rbtree.odin b/core/container/rbtree/rbtree.odin new file mode 100644 index 000000000..8ab131b3b --- /dev/null +++ b/core/container/rbtree/rbtree.odin @@ -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 +} \ No newline at end of file diff --git a/core/encoding/csv/example.odin b/core/encoding/csv/example.odin new file mode 100644 index 000000000..24722589d --- /dev/null +++ b/core/encoding/csv/example.odin @@ -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) + } + } +} \ No newline at end of file diff --git a/core/encoding/csv/reader.odin b/core/encoding/csv/reader.odin index f8c72c423..200bf43ea 100644 --- a/core/encoding/csv/reader.odin +++ b/core/encoding/csv/reader.odin @@ -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 - -} +} \ No newline at end of file diff --git a/core/encoding/json/marshal.odin b/core/encoding/json/marshal.odin index 83c0f8f79..4f5b50ec5 100644 --- a/core/encoding/json/marshal.odin +++ b/core/encoding/json/marshal.odin @@ -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 diff --git a/core/fmt/fmt.odin b/core/fmt/fmt.odin index 5f9485f7f..62cd95968 100644 --- a/core/fmt/fmt.odin +++ b/core/fmt/fmt.odin @@ -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) } diff --git a/core/image/qoi/qoi.odin b/core/image/qoi/qoi.odin index dfdf1875a..061943f68 100644 --- a/core/image/qoi/qoi.odin +++ b/core/image/qoi/qoi.odin @@ -1,381 +1,378 @@ -/* - Copyright 2022 Jeroen van Rijn . - 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.. (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 . + 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.. (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) } \ No newline at end of file diff --git a/core/odin/format/deprecated.odin b/core/odin/format/deprecated.odin new file mode 100644 index 000000000..3d8fea957 --- /dev/null +++ b/core/odin/format/deprecated.odin @@ -0,0 +1,3 @@ +package odin_format + +#panic("The format package has been deprecated. Please look at https://github.com/DanielGavin/ols") \ No newline at end of file diff --git a/core/odin/format/format.odin b/core/odin/format/format.odin deleted file mode 100644 index 66a7eb5f3..000000000 --- a/core/odin/format/format.odin +++ /dev/null @@ -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 -} diff --git a/core/odin/printer/deprecated.odin b/core/odin/printer/deprecated.odin new file mode 100644 index 000000000..6c053d1bc --- /dev/null +++ b/core/odin/printer/deprecated.odin @@ -0,0 +1,3 @@ +package odin_printer + +#panic("The printer package has been deprecated. Please look at https://github.com/DanielGavin/ols") \ No newline at end of file diff --git a/core/odin/printer/printer.odin b/core/odin/printer/printer.odin deleted file mode 100644 index ce75352bd..000000000 --- a/core/odin/printer/printer.odin +++ /dev/null @@ -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) - } - } - } -} diff --git a/core/odin/printer/visit.odin b/core/odin/printer/visit.odin deleted file mode 100644 index 571e4001d..000000000 --- a/core/odin/printer/visit.odin +++ /dev/null @@ -1,1629 +0,0 @@ -package odin_printer - -import "core:odin/ast" -import "core:odin/tokenizer" -import "core:strings" -import "core:fmt" -import "core:sort" - -//right now the attribute order is not linearly parsed(bug?) -@(private) -sort_attribute :: proc(s: ^[dynamic]^ast.Attribute) -> sort.Interface { - return sort.Interface { - collection = rawptr(s), - len = proc(it: sort.Interface) -> int { - s := (^[dynamic]^ast.Attribute)(it.collection) - return len(s^) - }, - less = proc(it: sort.Interface, i, j: int) -> bool { - s := (^[dynamic]^ast.Attribute)(it.collection) - return s[i].pos.offset < s[j].pos.offset - }, - swap = proc(it: sort.Interface, i, j: int) { - s := (^[dynamic]^ast.Attribute)(it.collection) - s[i], s[j] = s[j], s[i] - }, - } -} - -@(private) -comment_before_position :: proc(p: ^Printer, pos: tokenizer.Pos) -> bool { - if len(p.comments) <= p.latest_comment_index { - return false - } - - comment := p.comments[p.latest_comment_index] - - return comment.pos.offset < pos.offset -} - -@(private) -next_comment_group :: proc(p: ^Printer) { - p.latest_comment_index += 1 -} - -@(private) -push_comment :: proc(p: ^Printer, comment: tokenizer.Token) -> int { - if len(comment.text) == 0 { - return 0 - } - - if comment.text[:2] != "/*" { - format_token := Format_Token { - spaces_before = 1, - kind = .Comment, - text = comment.text, - } - - if len(p.current_line.format_tokens) == 0 { - format_token.spaces_before = 0 - } - - if !p.current_line.used { - p.current_line.used = true - p.current_line.depth = p.depth - } - - append(&p.current_line.format_tokens, format_token) - p.last_token = &p.current_line.format_tokens[len(p.current_line.format_tokens) - 1] - - hint_current_line(p, {.Line_Comment}) - - return 0 - } else { - builder := strings.builder_make(context.temp_allocator) - - c_len := len(comment.text) - trim_space := true - - multilines: [dynamic]string - - for i := 0; i < len(comment.text); i += 1 { - c := comment.text[i] - - if c != ' ' && c != '\t' { - trim_space = false - } - - switch { - case (c == ' ' || c == '\t' || c == '\n') && trim_space: - continue - case c == '\r' && comment.text[min(c_len - 1, i + 1)] == '\n': - append(&multilines, strings.to_string(builder)) - builder = strings.builder_make(context.temp_allocator) - trim_space = true - i += 1 - case c == '\n': - append(&multilines, strings.to_string(builder)) - builder = strings.builder_make(context.temp_allocator) - trim_space = true - case c == '/' && comment.text[min(c_len - 1, i + 1)] == '*': - strings.write_string(&builder, "/*") - trim_space = true - i += 1 - case c == '*' && comment.text[min(c_len - 1, i + 1)] == '/': - trim_space = true - strings.write_string(&builder, "*/") - i += 1 - case: - strings.write_byte(&builder, c) - } - } - - if strings.builder_len(builder) > 0 { - append(&multilines, strings.to_string(builder)) - } - - for line in multilines { - format_token := Format_Token { - spaces_before = 1, - kind = .Comment, - text = line, - } - - if len(p.current_line.format_tokens) == 0 { - format_token.spaces_before = 0 - } - - if strings.contains(line, "*/") { - unindent(p) - } - - if !p.current_line.used { - p.current_line.used = true - p.current_line.depth = p.depth - } - - append(&p.current_line.format_tokens, format_token) - p.last_token = &p.current_line.format_tokens[len(p.current_line.format_tokens) - 1] - - if strings.contains(line, "/*") { - indent(p) - } - - newline_position(p, 1) - } - - return len(multilines) - } -} - -@(private) -push_comments :: proc(p: ^Printer, pos: tokenizer.Pos) { - prev_comment: ^tokenizer.Token - prev_comment_lines: int - - for comment_before_position(p, pos) { - comment_group := p.comments[p.latest_comment_index] - - if prev_comment == nil { - lines := comment_group.pos.line - p.last_source_position.line - set_line(p, p.last_line_index + min(p.config.newline_limit+1, lines)) - } - - for comment, i in comment_group.list { - if prev_comment != nil && p.last_source_position.line != comment.pos.line { - newline_position(p, min(p.config.newline_limit+1, comment.pos.line - prev_comment.pos.line - prev_comment_lines)) - } - - prev_comment_lines = push_comment(p, comment) - prev_comment = &comment_group.list[i] - } - - next_comment_group(p) - } - - if prev_comment != nil { - newline_position(p, min(p.config.newline_limit+1, p.source_position.line - prev_comment.pos.line - prev_comment_lines)) - } -} - -@(private) -append_format_token :: proc(p: ^Printer, format_token: Format_Token) -> ^Format_Token { - format_token := format_token - - if p.last_token != nil && ( - p.last_token.kind == .Ellipsis || - p.last_token.kind == .Range_Half || p.last_token.kind == .Range_Full || - p.last_token.kind == .Open_Paren || p.last_token.kind == .Period || - p.last_token.kind == .Open_Brace || p.last_token.kind == .Open_Bracket) { - format_token.spaces_before = 0 - } else if p.merge_next_token { - format_token.spaces_before = 0 - p.merge_next_token = false - } else if p.space_next_token { - format_token.spaces_before = 1 - p.space_next_token = false - } - - push_comments(p, p.source_position) - - unwrapped_line := p.current_line - - if !unwrapped_line.used { - unwrapped_line.used = true - unwrapped_line.depth = p.depth - } - - if len(unwrapped_line.format_tokens) == 0 && format_token.spaces_before == 1 { - format_token.spaces_before = 0 - } - - p.last_source_position = p.source_position - p.last_line_index = p.current_line_index - - append(&unwrapped_line.format_tokens, format_token) - return &unwrapped_line.format_tokens[len(unwrapped_line.format_tokens) - 1] -} - -@(private) -push_format_token :: proc(p: ^Printer, format_token: Format_Token) { - p.last_token = append_format_token(p, format_token) -} - -@(private) -push_generic_token :: proc(p: ^Printer, kind: tokenizer.Token_Kind, spaces_before: int, value := "") { - format_token := Format_Token { - spaces_before = spaces_before, - kind = kind, - text = tokenizer.tokens[kind], - } - - if value != "" { - format_token.text = value - } - - p.last_token = append_format_token(p, format_token) -} - -@(private) -push_string_token :: proc(p: ^Printer, text: string, spaces_before: int) { - format_token := Format_Token { - spaces_before = spaces_before, - kind = .String, - text = text, - } - - p.last_token = append_format_token(p, format_token) -} - -@(private) -push_ident_token :: proc(p: ^Printer, text: string, spaces_before: int) { - format_token := Format_Token { - spaces_before = spaces_before, - kind = .Ident, - text = text, - } - - p.last_token = append_format_token(p, format_token) -} - -@(private) -set_source_position :: proc(p: ^Printer, pos: tokenizer.Pos) { - p.source_position = pos -} - -@(private) -move_line :: proc(p: ^Printer, pos: tokenizer.Pos) { - move_line_limit(p, pos, p.config.newline_limit+1) -} - -@(private) -move_line_limit :: proc(p: ^Printer, pos: tokenizer.Pos, limit: int) -> bool { - lines := min(pos.line - p.source_position.line, limit) - - if lines < 0 { - return false - } - - p.source_position = pos - p.current_line_index += lines - set_line(p, p.current_line_index) - return lines > 0 -} - -@(private) -set_line :: proc(p: ^Printer, line: int) -> ^Line { - unwrapped_line: ^Line - - if line >= len(p.lines) { - for i := len(p.lines); i <= line; i += 1 { - new_line: Line - new_line.format_tokens = make([dynamic]Format_Token, 0, 50, p.allocator) - append(&p.lines, new_line) - } - unwrapped_line = &p.lines[line] - } else { - unwrapped_line = &p.lines[line] - } - - p.current_line = unwrapped_line - p.current_line_index = line - - return unwrapped_line -} - -@(private) -newline_position :: proc(p: ^Printer, count: int) { - p.current_line_index += count - set_line(p, p.current_line_index) -} - -@(private) -indent :: proc(p: ^Printer) { - p.depth += 1 -} - -@(private) -unindent :: proc(p: ^Printer) { - p.depth -= 1 -} - -@(private) -merge_next_token :: proc(p: ^Printer) { - p.merge_next_token = true -} - -@(private) -space_next_token :: proc(p: ^Printer) { - p.space_next_token = true -} - -@(private) -hint_current_line :: proc(p: ^Printer, hint: Line_Type) { - p.current_line.types |= hint -} - -@(private) -visit_decl :: proc(p: ^Printer, decl: ^ast.Decl, called_in_stmt := false) { - if decl == nil { - return - } - - #partial switch v in decl.derived_stmt { - case ^ast.Expr_Stmt: - move_line(p, decl.pos) - visit_expr(p, v.expr) - if p.config.semicolons { - push_generic_token(p, .Semicolon, 0) - } - case ^ast.When_Stmt: - visit_stmt(p, cast(^ast.Stmt)decl) - case ^ast.Foreign_Import_Decl: - if len(v.attributes) > 0 { - sort.sort(sort_attribute(&v.attributes)) - move_line(p, v.attributes[0].pos) - visit_attributes(p, v.attributes) - } - - move_line(p, decl.pos) - - push_generic_token(p, v.foreign_tok.kind, 0) - push_generic_token(p, v.import_tok.kind, 1) - - if v.name != nil { - push_ident_token(p, v.name.name, 1) - } - - for path in v.fullpaths { - push_ident_token(p, path, 0) - } - case ^ast.Foreign_Block_Decl: - if len(v.attributes) > 0 { - sort.sort(sort_attribute(&v.attributes)) - move_line(p, v.attributes[0].pos) - visit_attributes(p, v.attributes) - } - - move_line(p, decl.pos) - - push_generic_token(p, .Foreign, 0) - - visit_expr(p, v.foreign_library) - visit_stmt(p, v.body) - case ^ast.Import_Decl: - move_line(p, decl.pos) - - if v.name.text != "" { - push_generic_token(p, v.import_tok.kind, 1) - push_generic_token(p, v.name.kind, 1, v.name.text) - push_ident_token(p, v.fullpath, 1) - } else { - push_generic_token(p, v.import_tok.kind, 1) - push_ident_token(p, v.fullpath, 1) - } - - case ^ast.Value_Decl: - if len(v.attributes) > 0 { - sort.sort(sort_attribute(&v.attributes)) - move_line(p, v.attributes[0].pos) - visit_attributes(p, v.attributes) - } - - move_line(p, decl.pos) - - if v.is_using { - push_generic_token(p, .Using, 0) - } - - visit_exprs(p, v.names, {.Add_Comma}) - - hint_current_line(p, {.Value_Decl}) - - if v.type != nil { - if !v.is_mutable { - push_generic_token(p, .Colon, 0) - } else { - push_generic_token(p, .Colon, 0) - } - - visit_expr(p, v.type) - } else { - if !v.is_mutable { - push_generic_token(p, .Colon, 1) - push_generic_token(p, .Colon, 0) - } else { - push_generic_token(p, .Colon, 1) - } - } - - if v.is_mutable && v.type != nil && len(v.values) != 0 { - push_generic_token(p, .Eq, 1) - } else if v.is_mutable && v.type == nil && len(v.values) != 0 { - push_generic_token(p, .Eq, 0) - } else if !v.is_mutable && v.type != nil { - push_generic_token(p, .Colon, 0) - } - - if len(v.values) == 1 { - visit_expr(p, v.values[0]) //this is too ensure that one value are never newlined(procs, structs, etc.) - } else { - visit_exprs(p, v.values, {.Add_Comma}) - } - - add_semicolon := true - - for value in v.values { - #partial switch a in value.derived { - case ^ast.Union_Type, ^ast.Enum_Type, ^ast.Struct_Type, ^ast.Bit_Field_Type: - add_semicolon = false || called_in_stmt - case ^ast.Proc_Lit: - add_semicolon = false - } - } - - if add_semicolon && p.config.semicolons && !p.skip_semicolon { - push_generic_token(p, .Semicolon, 0) - } - - case: - panic(fmt.aprint(decl.derived)) - } -} - -@(private) -visit_exprs :: proc(p: ^Printer, list: []^ast.Expr, options := List_Options{}) { - if len(list) == 0 { - return - } - - // we have to newline the expressions to respect the source - for expr, i in list { - // Don't move the first expression, it looks bad - if i != 0 && .Enforce_Newline in options { - newline_position(p, 1) - } else if i != 0 { - move_line_limit(p, expr.pos, 1) - } - - visit_expr(p, expr, options) - - if (i != len(list) - 1 || .Trailing in options) && .Add_Comma in options { - push_generic_token(p, .Comma, 0) - } - } - - if len(list) > 1 && .Enforce_Newline in options { - newline_position(p, 1) - } -} - -@(private) -visit_bit_field_fields :: proc(p: ^Printer, list: []^ast.Bit_Field_Field, options := List_Options{}) { - if len(list) == 0 { - return - } - - // we have to newline the expressions to respect the source - for v, i in list { - // Don't move the first expression, it looks bad - if i != 0 && .Enforce_Newline in options { - newline_position(p, 1) - } else if i != 0 { - move_line_limit(p, v.pos, 1) - } - - visit_expr(p, v.name, options) - push_generic_token(p, .Colon, 0) - visit_expr(p, v.type, options) - push_generic_token(p, .Or, 1) - visit_expr(p, v.bit_size, options) - - if (i != len(list) - 1 || .Trailing in options) && .Add_Comma in options { - push_generic_token(p, .Comma, 0) - } - } - - if len(list) > 1 && .Enforce_Newline in options { - newline_position(p, 1) - } -} - -@(private) -visit_attributes :: proc(p: ^Printer, attributes: [dynamic]^ast.Attribute) { - if len(attributes) == 0 { - return - } - - for attribute in attributes { - move_line_limit(p, attribute.pos, 1) - - push_generic_token(p, .At, 0) - push_generic_token(p, .Open_Paren, 0) - - visit_exprs(p, attribute.elems, {.Add_Comma}) - - push_generic_token(p, .Close_Paren, 0) - } -} - -@(private) -visit_stmt :: proc(p: ^Printer, stmt: ^ast.Stmt, block_type: Block_Type = .Generic, empty_block := false, block_stmt := false) { - if stmt == nil { - return - } - - - switch v in stmt.derived_stmt { - case ^ast.Bad_Stmt: - case ^ast.Bad_Decl: - case ^ast.Package_Decl: - - case ^ast.Empty_Stmt: - push_generic_token(p, .Semicolon, 0) - case ^ast.Tag_Stmt: - push_generic_token(p, .Hash, 1) - push_generic_token(p, v.op.kind, 1, v.op.text) - visit_stmt(p, v.stmt) - - - case ^ast.Import_Decl: - visit_decl(p, cast(^ast.Decl)stmt, true) - return - case ^ast.Value_Decl: - visit_decl(p, cast(^ast.Decl)stmt, true) - return - case ^ast.Foreign_Import_Decl: - visit_decl(p, cast(^ast.Decl)stmt, true) - return - case ^ast.Foreign_Block_Decl: - visit_decl(p, cast(^ast.Decl)stmt, true) - return - - case ^ast.Using_Stmt: - move_line(p, v.pos) - - push_generic_token(p, .Using, 1) - - visit_exprs(p, v.list, {.Add_Comma}) - - if p.config.semicolons { - push_generic_token(p, .Semicolon, 0) - } - case ^ast.Block_Stmt: - move_line(p, v.pos) - - if v.pos.line == v.end.line { - if !empty_block { - push_generic_token(p, .Open_Brace, 1) - } - - set_source_position(p, v.pos) - - visit_block_stmts(p, v.stmts, len(v.stmts) > 1 && p.config.split_multiple_stmts) - - set_source_position(p, v.end) - - if !empty_block { - push_generic_token(p, .Close_Brace, 0) - } - } else { - if !empty_block { - visit_begin_brace(p, v.pos, block_type, len(v.stmts)) - } - - set_source_position(p, v.pos) - - visit_block_stmts(p, v.stmts, len(v.stmts) > 1 && p.config.split_multiple_stmts) - - if !empty_block { - visit_end_brace(p, v.end) - } - } - case ^ast.If_Stmt: - move_line(p, v.pos) - - if v.label != nil { - visit_expr(p, v.label) - push_generic_token(p, .Colon, 0) - } - - push_generic_token(p, .If, 1) - - hint_current_line(p, {.If}) - - if v.init != nil { - p.skip_semicolon = true - visit_stmt(p, v.init) - p.skip_semicolon = false - push_generic_token(p, .Semicolon, 0) - } - - visit_expr(p, v.cond) - - uses_do := false - - if check_stmt, ok := v.body.derived.(^ast.Block_Stmt); ok && check_stmt.uses_do { - uses_do = true - } - - if uses_do && !p.config.convert_do { - push_generic_token(p, .Do, 1) - visit_stmt(p, v.body, .If_Stmt, true) - } else { - if uses_do { - newline_position(p, 1) - } - - set_source_position(p, v.body.pos) - - visit_stmt(p, v.body, .If_Stmt) - - set_source_position(p, v.body.end) - } - - if v.else_stmt != nil { - - if p.config.brace_style == .Allman || p.config.brace_style == .Stroustrup { - newline_position(p, 1) - } - - push_generic_token(p, .Else, 1) - - set_source_position(p, v.else_stmt.pos) - - visit_stmt(p, v.else_stmt) - } - case ^ast.Switch_Stmt: - move_line(p, v.pos) - - if v.label != nil { - visit_expr(p, v.label) - push_generic_token(p, .Colon, 0) - } - - if v.partial { - push_ident_token(p, "#partial", 1) - } - - push_generic_token(p, .Switch, 1) - - hint_current_line(p, {.Switch_Stmt}) - - if v.init != nil { - p.skip_semicolon = true - visit_stmt(p, v.init) - p.skip_semicolon = false - } - - if v.init != nil && v.cond != nil { - push_generic_token(p, .Semicolon, 0) - } - - visit_expr(p, v.cond) - visit_stmt(p, v.body) - case ^ast.Case_Clause: - move_line(p, v.pos) - - if !p.config.indent_cases { - unindent(p) - } - - push_generic_token(p, .Case, 0) - - if v.list != nil { - visit_exprs(p, v.list, {.Add_Comma}) - } - - push_generic_token(p, v.terminator.kind, 0) - - indent(p) - - visit_block_stmts(p, v.body) - - unindent(p) - - if !p.config.indent_cases { - indent(p) - } - case ^ast.Type_Switch_Stmt: - move_line(p, v.pos) - - hint_current_line(p, {.Switch_Stmt}) - - if v.label != nil { - visit_expr(p, v.label) - push_generic_token(p, .Colon, 0) - } - - if v.partial { - push_ident_token(p, "#partial", 1) - } - - push_generic_token(p, .Switch, 1) - - visit_stmt(p, v.tag) - visit_stmt(p, v.body) - case ^ast.Assign_Stmt: - move_line(p, v.pos) - - hint_current_line(p, {.Assign}) - - visit_exprs(p, v.lhs, {.Add_Comma}) - - push_generic_token(p, v.op.kind, 1) - - visit_exprs(p, v.rhs, {.Add_Comma}) - - if block_stmt && p.config.semicolons { - push_generic_token(p, .Semicolon, 0) - } - case ^ast.Expr_Stmt: - move_line(p, v.pos) - visit_expr(p, v.expr) - if block_stmt && p.config.semicolons { - push_generic_token(p, .Semicolon, 0) - } - case ^ast.For_Stmt: - // this should be simplified - move_line(p, v.pos) - - if v.label != nil { - visit_expr(p, v.label) - push_generic_token(p, .Colon, 0) - } - - push_generic_token(p, .For, 1) - - hint_current_line(p, {.For}) - - if v.init != nil { - p.skip_semicolon = true - visit_stmt(p, v.init) - p.skip_semicolon = false - push_generic_token(p, .Semicolon, 0) - } else if v.post != nil { - push_generic_token(p, .Semicolon, 0) - } - - if v.cond != nil { - move_line(p, v.cond.pos) - visit_expr(p, v.cond) - } - - if v.post != nil { - push_generic_token(p, .Semicolon, 0) - move_line(p, v.post.pos) - visit_stmt(p, v.post) - } else if v.post == nil && v.cond != nil && v.init != nil { - push_generic_token(p, .Semicolon, 0) - } - - visit_stmt(p, v.body) - - case ^ast.Inline_Range_Stmt: - move_line(p, v.pos) - - if v.label != nil { - visit_expr(p, v.label) - push_generic_token(p, .Colon, 0) - } - - push_ident_token(p, "#unroll", 0) - - push_generic_token(p, .For, 1) - - hint_current_line(p, {.For}) - - visit_expr(p, v.val0) - - if v.val1 != nil { - push_generic_token(p, .Comma, 0) - visit_expr(p, v.val1) - } - - push_generic_token(p, .In, 1) - - visit_expr(p, v.expr) - visit_stmt(p, v.body) - - case ^ast.Range_Stmt: - move_line(p, v.pos) - - if v.label != nil { - visit_expr(p, v.label) - push_generic_token(p, .Colon, 0) - } - - push_generic_token(p, .For, 1) - - hint_current_line(p, {.For}) - - if len(v.vals) >= 1 { - visit_expr(p, v.vals[0]) - } - - if len(v.vals) >= 2 { - push_generic_token(p, .Comma, 0) - visit_expr(p, v.vals[1]) - } - - push_generic_token(p, .In, 1) - - visit_expr(p, v.expr) - - visit_stmt(p, v.body) - case ^ast.Return_Stmt: - move_line(p, v.pos) - - push_generic_token(p, .Return, 1) - - if v.results != nil { - visit_exprs(p, v.results, {.Add_Comma}) - } - - if block_stmt && p.config.semicolons { - push_generic_token(p, .Semicolon, 0) - } - case ^ast.Defer_Stmt: - move_line(p, v.pos) - push_generic_token(p, .Defer, 0) - - visit_stmt(p, v.stmt) - - if p.config.semicolons { - push_generic_token(p, .Semicolon, 0) - } - case ^ast.When_Stmt: - move_line(p, v.pos) - push_generic_token(p, .When, 1) - visit_expr(p, v.cond) - - visit_stmt(p, v.body) - - if v.else_stmt != nil { - - if p.config.brace_style == .Allman { - newline_position(p, 1) - } - - push_generic_token(p, .Else, 1) - - set_source_position(p, v.else_stmt.pos) - - visit_stmt(p, v.else_stmt) - } - - case ^ast.Branch_Stmt: - move_line(p, v.pos) - - push_generic_token(p, v.tok.kind, 0) - - if v.label != nil { - visit_expr(p, v.label) - } - - if p.config.semicolons { - push_generic_token(p, .Semicolon, 0) - } - case: - panic(fmt.aprint(stmt.derived)) - } - - set_source_position(p, stmt.end) -} - -@(private) -push_where_clauses :: proc(p: ^Printer, clauses: []^ast.Expr) { - if len(clauses) == 0 { - return - } - - // TODO(bill): This is not outputting correctly at all - - move_line(p, clauses[0].pos) - push_generic_token(p, .Where, 1) - - force_newline := false - - for expr, i in clauses { - // Don't move the first expression, it looks bad - if i != 0 && i != len(clauses)-1 && force_newline { - newline_position(p, 1) - } else if i != 0 { - move_line_limit(p, expr.pos, 1) - } - - visit_expr(p, expr) - - if i != len(clauses) - 1 { - push_generic_token(p, .Comma, 0) - } - } - - if len(clauses) > 1 && force_newline { - newline_position(p, 1) - } -} - -@(private) -push_poly_params :: proc(p: ^Printer, poly_params: ^ast.Field_List) { - if poly_params != nil { - push_generic_token(p, .Open_Paren, 0) - visit_field_list(p, poly_params, {.Add_Comma, .Enforce_Poly_Names}) - push_generic_token(p, .Close_Paren, 0) - } -} - - -@(private) -visit_expr :: proc(p: ^Printer, expr: ^ast.Expr, options := List_Options{}) { - if expr == nil { - return - } - - set_source_position(p, expr.pos) - - switch v in expr.derived_expr { - case ^ast.Bad_Expr: - - case ^ast.Tag_Expr: - push_generic_token(p, .Hash, 1) - push_generic_token(p, v.op.kind, 1, v.op.text) - visit_expr(p, v.expr) - - case ^ast.Inline_Asm_Expr: - push_generic_token(p, v.tok.kind, 1, v.tok.text) - - push_generic_token(p, .Open_Paren, 1) - visit_exprs(p, v.param_types, {.Add_Comma}) - push_generic_token(p, .Close_Paren, 0) - - push_generic_token(p, .Sub, 1) - push_generic_token(p, .Gt, 0) - - visit_expr(p, v.return_type) - - push_generic_token(p, .Open_Brace, 1) - visit_expr(p, v.asm_string) - push_generic_token(p, .Comma, 0) - visit_expr(p, v.constraints_string) - push_generic_token(p, .Close_Brace, 0) - case ^ast.Undef: - push_generic_token(p, .Undef, 1) - case ^ast.Auto_Cast: - push_generic_token(p, v.op.kind, 1) - visit_expr(p, v.expr) - case ^ast.Ternary_If_Expr: - visit_expr(p, v.x) - push_generic_token(p, v.op1.kind, 1) - visit_expr(p, v.cond) - push_generic_token(p, v.op2.kind, 1) - visit_expr(p, v.y) - case ^ast.Ternary_When_Expr: - visit_expr(p, v.x) - push_generic_token(p, v.op1.kind, 1) - visit_expr(p, v.cond) - push_generic_token(p, v.op2.kind, 1) - visit_expr(p, v.y) - case ^ast.Or_Else_Expr: - visit_expr(p, v.x) - push_generic_token(p, v.token.kind, 1) - visit_expr(p, v.y) - case ^ast.Or_Return_Expr: - visit_expr(p, v.expr) - push_generic_token(p, v.token.kind, 1) - case ^ast.Or_Branch_Expr: - visit_expr(p, v.expr) - push_generic_token(p, v.token.kind, 1) - if v.label != nil { - visit_expr(p, v.label) - } - - case ^ast.Selector_Call_Expr: - visit_expr(p, v.call.expr) - push_generic_token(p, .Open_Paren, 1) - visit_exprs(p, v.call.args, {.Add_Comma}) - push_generic_token(p, .Close_Paren, 0) - case ^ast.Ellipsis: - push_generic_token(p, .Ellipsis, 1) - visit_expr(p, v.expr) - case ^ast.Relative_Type: - visit_expr(p, v.tag) - visit_expr(p, v.type) - case ^ast.Slice_Expr: - visit_expr(p, v.expr) - push_generic_token(p, .Open_Bracket, 0) - visit_expr(p, v.low) - push_generic_token(p, v.interval.kind, 0) - if v.high != nil { - merge_next_token(p) - visit_expr(p, v.high) - } - push_generic_token(p, .Close_Bracket, 0) - case ^ast.Ident: - if .Enforce_Poly_Names in options { - push_generic_token(p, .Dollar, 1) - push_ident_token(p, v.name, 0) - } else { - push_ident_token(p, v.name, 1) - } - case ^ast.Deref_Expr: - visit_expr(p, v.expr) - push_generic_token(p, v.op.kind, 0) - case ^ast.Type_Cast: - push_generic_token(p, v.tok.kind, 1) - push_generic_token(p, .Open_Paren, 0) - visit_expr(p, v.type) - push_generic_token(p, .Close_Paren, 0) - merge_next_token(p) - visit_expr(p, v.expr) - case ^ast.Basic_Directive: - push_generic_token(p, v.tok.kind, 1) - push_ident_token(p, v.name, 0) - case ^ast.Distinct_Type: - push_generic_token(p, .Distinct, 1) - visit_expr(p, v.type) - case ^ast.Dynamic_Array_Type: - visit_expr(p, v.tag) - push_generic_token(p, .Open_Bracket, 1) - push_generic_token(p, .Dynamic, 0) - push_generic_token(p, .Close_Bracket, 0) - merge_next_token(p) - visit_expr(p, v.elem) - case ^ast.Bit_Set_Type: - push_generic_token(p, .Bit_Set, 1) - push_generic_token(p, .Open_Bracket, 0) - - visit_expr(p, v.elem) - - if v.underlying != nil { - push_generic_token(p, .Semicolon, 0) - visit_expr(p, v.underlying) - } - - push_generic_token(p, .Close_Bracket, 0) - case ^ast.Union_Type: - push_generic_token(p, .Union, 1) - - push_poly_params(p, v.poly_params) - - switch v.kind { - case .Normal: - case .maybe: push_ident_token(p, "#maybe", 1) - case .no_nil: push_ident_token(p, "#no_nil", 1) - case .shared_nil: push_ident_token(p, "#shared_nil", 1) - } - - push_where_clauses(p, v.where_clauses) - - if v.variants != nil && (len(v.variants) == 0 || v.pos.line == v.end.line) { - push_generic_token(p, .Open_Brace, 1) - visit_exprs(p, v.variants, {.Add_Comma}) - push_generic_token(p, .Close_Brace, 0) - } else { - visit_begin_brace(p, v.pos, .Generic) - newline_position(p, 1) - set_source_position(p, v.variants[0].pos) - visit_exprs(p, v.variants, {.Add_Comma, .Trailing}) - visit_end_brace(p, v.end) - } - case ^ast.Enum_Type: - push_generic_token(p, .Enum, 1) - - hint_current_line(p, {.Enum}) - - if v.base_type != nil { - visit_expr(p, v.base_type) - } - - if v.fields != nil && (len(v.fields) == 0 || v.pos.line == v.end.line) { - push_generic_token(p, .Open_Brace, 1) - visit_exprs(p, v.fields, {.Add_Comma}) - push_generic_token(p, .Close_Brace, 0) - } else { - visit_begin_brace(p, v.pos, .Generic, len(v.fields)) - newline_position(p, 1) - set_source_position(p, v.fields[0].pos) - visit_exprs(p, v.fields, {.Add_Comma, .Trailing, .Enforce_Newline}) - set_source_position(p, v.end) - visit_end_brace(p, v.end) - } - - set_source_position(p, v.end) - case ^ast.Struct_Type: - push_generic_token(p, .Struct, 1) - - hint_current_line(p, {.Struct}) - - push_poly_params(p, v.poly_params) - - if v.is_packed { - push_ident_token(p, "#packed", 1) - } - - if v.is_raw_union { - push_ident_token(p, "#raw_union", 1) - } - - if v.align != nil { - push_ident_token(p, "#align", 1) - visit_expr(p, v.align) - } - - push_where_clauses(p, v.where_clauses) - - if v.fields != nil && (len(v.fields.list) == 0 || v.pos.line == v.end.line) { - push_generic_token(p, .Open_Brace, 1) - set_source_position(p, v.fields.pos) - visit_field_list(p, v.fields, {.Add_Comma}) - push_generic_token(p, .Close_Brace, 0) - } else if v.fields != nil { - visit_begin_brace(p, v.pos, .Generic, len(v.fields.list)) - set_source_position(p, v.fields.pos) - visit_field_list(p, v.fields, {.Add_Comma, .Trailing, .Enforce_Newline}) - visit_end_brace(p, v.end) - } - - set_source_position(p, v.end) - case ^ast.Proc_Lit: - switch v.inlining { - case .None: - case .Inline: - push_ident_token(p, "#force_inline", 0) - case .No_Inline: - push_ident_token(p, "#force_no_inline", 0) - } - - visit_proc_type(p, v.type, true) - - push_where_clauses(p, v.where_clauses) - - if v.body != nil { - set_source_position(p, v.body.pos) - visit_stmt(p, v.body, .Proc) - } else { - push_generic_token(p, .Undef, 1) - } - case ^ast.Proc_Type: - visit_proc_type(p, v) - case ^ast.Basic_Lit: - push_generic_token(p, v.tok.kind, 1, v.tok.text) - case ^ast.Binary_Expr: - visit_binary_expr(p, v) - case ^ast.Implicit_Selector_Expr: - push_generic_token(p, .Period, 1) - push_ident_token(p, v.field.name, 0) - case ^ast.Call_Expr: - visit_expr(p, v.expr) - - push_format_token(p, - Format_Token { - kind = .Open_Paren, - type = .Call, - text = "(", - }, - ) - - hint_current_line(p, {.Call}) - - visit_call_exprs(p, v.args, v.ellipsis.kind == .Ellipsis) - push_generic_token(p, .Close_Paren, 0) - case ^ast.Typeid_Type: - push_generic_token(p, .Typeid, 1) - - if v.specialization != nil { - push_generic_token(p, .Quo, 0) - visit_expr(p, v.specialization) - } - case ^ast.Selector_Expr: - visit_expr(p, v.expr) - push_generic_token(p, v.op.kind, 0) - visit_expr(p, v.field) - case ^ast.Paren_Expr: - push_generic_token(p, .Open_Paren, 1) - visit_expr(p, v.expr) - push_generic_token(p, .Close_Paren, 0) - case ^ast.Index_Expr: - visit_expr(p, v.expr) - push_generic_token(p, .Open_Bracket, 0) - visit_expr(p, v.index) - push_generic_token(p, .Close_Bracket, 0) - case ^ast.Matrix_Index_Expr: - visit_expr(p, v.expr) - push_generic_token(p, .Open_Bracket, 0) - visit_expr(p, v.row_index) - push_generic_token(p, .Comma, 0) - visit_expr(p, v.column_index) - push_generic_token(p, .Close_Bracket, 0) - case ^ast.Proc_Group: - push_generic_token(p, v.tok.kind, 1) - - if len(v.args) != 0 && v.pos.line != v.args[len(v.args) - 1].pos.line { - visit_begin_brace(p, v.pos, .Generic) - newline_position(p, 1) - set_source_position(p, v.args[0].pos) - visit_exprs(p, v.args, {.Add_Comma, .Trailing}) - visit_end_brace(p, v.end) - } else { - push_generic_token(p, .Open_Brace, 0) - visit_exprs(p, v.args, {.Add_Comma}) - push_generic_token(p, .Close_Brace, 0) - } - - case ^ast.Comp_Lit: - if v.type != nil { - visit_expr(p, v.type) - } - - if len(v.elems) != 0 && v.pos.line != v.elems[len(v.elems) - 1].pos.line { - visit_begin_brace(p, v.pos, .Comp_Lit, 0) - newline_position(p, 1) - set_source_position(p, v.elems[0].pos) - visit_exprs(p, v.elems, {.Add_Comma, .Trailing}) - visit_end_brace(p, v.end) - } else { - push_generic_token(p, .Open_Brace, 0 if v.type != nil else 1) - visit_exprs(p, v.elems, {.Add_Comma}) - push_generic_token(p, .Close_Brace, 0) - } - - case ^ast.Unary_Expr: - push_generic_token(p, v.op.kind, 1) - merge_next_token(p) - visit_expr(p, v.expr) - case ^ast.Field_Value: - visit_expr(p, v.field) - push_generic_token(p, .Eq, 1) - visit_expr(p, v.value) - case ^ast.Type_Assertion: - visit_expr(p, v.expr) - - if unary, ok := v.type.derived.(^ast.Unary_Expr); ok && unary.op.text == "?" { - push_generic_token(p, .Period, 0) - visit_expr(p, v.type) - } else { - push_generic_token(p, .Period, 0) - push_generic_token(p, .Open_Paren, 0) - visit_expr(p, v.type) - push_generic_token(p, .Close_Paren, 0) - } - - case ^ast.Pointer_Type: - push_generic_token(p, .Pointer, 1) - merge_next_token(p) - visit_expr(p, v.elem) - case ^ast.Implicit: - push_generic_token(p, v.tok.kind, 1) - case ^ast.Poly_Type: - push_generic_token(p, .Dollar, 1) - merge_next_token(p) - visit_expr(p, v.type) - - if v.specialization != nil { - push_generic_token(p, .Quo, 0) - merge_next_token(p) - visit_expr(p, v.specialization) - } - case ^ast.Array_Type: - visit_expr(p, v.tag) - push_generic_token(p, .Open_Bracket, 1) - visit_expr(p, v.len) - push_generic_token(p, .Close_Bracket, 0) - merge_next_token(p) - visit_expr(p, v.elem) - case ^ast.Map_Type: - push_generic_token(p, .Map, 1) - push_generic_token(p, .Open_Bracket, 0) - visit_expr(p, v.key) - push_generic_token(p, .Close_Bracket, 0) - merge_next_token(p) - visit_expr(p, v.value) - case ^ast.Helper_Type: - visit_expr(p, v.type) - case ^ast.Multi_Pointer_Type: - push_generic_token(p, .Open_Bracket, 1) - push_generic_token(p, .Pointer, 0) - push_generic_token(p, .Close_Bracket, 0) - visit_expr(p, v.elem) - case ^ast.Matrix_Type: - push_generic_token(p, .Matrix, 1) - push_generic_token(p, .Open_Bracket, 0) - visit_expr(p, v.row_count) - push_generic_token(p, .Comma, 0) - visit_expr(p, v.column_count) - push_generic_token(p, .Close_Bracket, 0) - visit_expr(p, v.elem) - case ^ast.Bit_Field_Type: - push_generic_token(p, .Bit_Field, 1) - - visit_expr(p, v.backing_type) - - if len(v.fields) == 0 || v.pos.line == v.close.line { - push_generic_token(p, .Open_Brace, 1) - visit_bit_field_fields(p, v.fields, {.Add_Comma}) - push_generic_token(p, .Close_Brace, 0) - } else { - visit_begin_brace(p, v.pos, .Generic, len(v.fields)) - newline_position(p, 1) - set_source_position(p, v.fields[0].pos) - visit_bit_field_fields(p, v.fields, {.Add_Comma, .Trailing, .Enforce_Newline}) - set_source_position(p, v.close) - visit_end_brace(p, v.close) - } - - set_source_position(p, v.close) - case: - panic(fmt.aprint(expr.derived)) - } -} - -visit_begin_brace :: proc(p: ^Printer, begin: tokenizer.Pos, type: Block_Type, count := 0, same_line_spaces_before := 1) { - set_source_position(p, begin) - - newline_braced := p.config.brace_style == .Allman - newline_braced |= p.config.brace_style == .K_And_R && type == .Proc - newline_braced &= p.config.brace_style != ._1TBS - - format_token := Format_Token { - kind = .Open_Brace, - parameter_count = count, - text = "{", - } - - if newline_braced { - newline_position(p, 1) - push_format_token(p, format_token) - indent(p) - } else { - format_token.spaces_before = same_line_spaces_before - push_format_token(p, format_token) - indent(p) - } -} - -visit_end_brace :: proc(p: ^Printer, end: tokenizer.Pos) { - move_line(p, end) - push_generic_token(p, .Close_Brace, 0) - unindent(p) - p.current_line.depth = p.depth -} - -visit_block_stmts :: proc(p: ^Printer, stmts: []^ast.Stmt, split := false) { - for stmt, i in stmts { - visit_stmt(p, stmt, .Generic, false, true) - - if split && i != len(stmts) - 1 && stmt.pos.line == stmts[i + 1].pos.line { - newline_position(p, 1) - } - } -} - -List_Option :: enum u8 { - Add_Comma, - Trailing, - Enforce_Newline, - Enforce_Poly_Names, -} - -List_Options :: distinct bit_set[List_Option] - -visit_field_list :: proc(p: ^Printer, list: ^ast.Field_List, options := List_Options{}) { - if list.list == nil { - return - } - - for field, i in list.list { - if !move_line_limit(p, field.pos, 1) && .Enforce_Newline in options { - newline_position(p, 1) - } - - if .Using in field.flags { - push_generic_token(p, .Using, 1) - } - - name_options := List_Options{.Add_Comma} - if .Enforce_Poly_Names in options { - name_options += {.Enforce_Poly_Names} - } - - visit_exprs(p, field.names, name_options) - - if field.type != nil { - if len(field.names) != 0 { - push_generic_token(p, .Colon, 0) - } - visit_expr(p, field.type) - } else { - push_generic_token(p, .Colon, 1) - push_generic_token(p, .Eq, 0) - visit_expr(p, field.default_value) - } - - if field.tag.text != "" { - push_generic_token(p, field.tag.kind, 1, field.tag.text) - } - - if (i != len(list.list) - 1 || .Trailing in options) && .Add_Comma in options { - push_generic_token(p, .Comma, 0) - } - } -} - -visit_proc_type :: proc(p: ^Printer, proc_type: ^ast.Proc_Type, is_proc_lit := false) { - if is_proc_lit { - push_format_token(p, Format_Token { - kind = .Proc, - type = .Proc_Lit, - text = "proc", - spaces_before = 1, - }) - } else { - push_format_token(p, Format_Token { - kind = .Proc, - text = "proc", - spaces_before = 1, - }) - } - - explicit_calling := false - - if v, ok := proc_type.calling_convention.(string); ok { - explicit_calling = true - push_string_token(p, v, 1) - } - - if explicit_calling { - push_generic_token(p, .Open_Paren, 1) - } else { - push_generic_token(p, .Open_Paren, 0) - } - - visit_signature_list(p, proc_type.params, false) - - push_generic_token(p, .Close_Paren, 0) - - if proc_type.results != nil { - push_generic_token(p, .Sub, 1) - push_generic_token(p, .Gt, 0) - - use_parens := false - - if len(proc_type.results.list) > 1 { - use_parens = true - } else if len(proc_type.results.list) == 1 { - - for name in proc_type.results.list[0].names { - if ident, ok := name.derived.(^ast.Ident); ok { - if ident.name != "_" { - use_parens = true - } - } - } - } - - if use_parens { - push_generic_token(p, .Open_Paren, 1) - visit_signature_list(p, proc_type.results) - push_generic_token(p, .Close_Paren, 0) - } else { - visit_signature_list(p, proc_type.results) - } - } -} - -visit_binary_expr :: proc(p: ^Printer, binary: ^ast.Binary_Expr) { - move_line(p, binary.left.pos) - - if v, ok := binary.left.derived.(^ast.Binary_Expr); ok { - visit_binary_expr(p, v) - } else { - visit_expr(p, binary.left) - } - - either_implicit_selector := false - if _, lok := binary.left.derived.(^ast.Implicit_Selector_Expr); lok { - either_implicit_selector = true - } else if _, rok := binary.right.derived.(^ast.Implicit_Selector_Expr); rok { - either_implicit_selector = true - } - - #partial switch binary.op.kind { - case .Ellipsis: - push_generic_token(p, binary.op.kind, 1 if either_implicit_selector else 0, - tokenizer.tokens[tokenizer.Token_Kind.Range_Full]) - case .Range_Half, .Range_Full: - push_generic_token(p, binary.op.kind, 1 if either_implicit_selector else 0) - case: - push_generic_token(p, binary.op.kind, 1) - } - - move_line(p, binary.right.pos) - - - if v, ok := binary.right.derived.(^ast.Binary_Expr); ok { - visit_binary_expr(p, v) - } else { - visit_expr(p, binary.right) - } -} - -visit_call_exprs :: proc(p: ^Printer, list: []^ast.Expr, ellipsis := false) { - if len(list) == 0 { - return - } - - // all the expression are on the line - if list[0].pos.line == list[len(list) - 1].pos.line { - for expr, i in list { - if i == len(list) - 1 && ellipsis { - push_generic_token(p, .Ellipsis, 0) - } - - visit_expr(p, expr) - - if i != len(list) - 1 { - push_generic_token(p, .Comma, 0) - } - } - } else { - for expr, i in list { - // we have to newline the expressions to respect the source - move_line_limit(p, expr.pos, 1) - - if i == len(list) - 1 && ellipsis { - push_generic_token(p, .Ellipsis, 0) - } - - visit_expr(p, expr) - - if i != len(list) - 1 { - push_generic_token(p, .Comma, 0) - } - } - } -} - -visit_signature_list :: proc(p: ^Printer, list: ^ast.Field_List, remove_blank := true) { - if list.list == nil { - return - } - - for field, i in list.list { - if i != 0 { - move_line_limit(p, field.pos, 1) - } - - if .Using in field.flags { - push_generic_token(p, .Using, 0) - } - - named := false - - for name in field.names { - if ident, ok := name.derived.(^ast.Ident); ok { - //for some reason the parser uses _ to mean empty - if ident.name != "_" || !remove_blank { - named = true - } - } else { - //alternative is poly names - named = true - } - } - - if named { - visit_exprs(p, field.names, {.Add_Comma}) - - if len(field.names) != 0 && field.type != nil { - push_generic_token(p, .Colon, 0) - } - } - - if field.type != nil && field.default_value != nil { - visit_expr(p, field.type) - push_generic_token(p, .Eq, 1) - visit_expr(p, field.default_value) - } else if field.type != nil { - visit_expr(p, field.type) - } else { - push_generic_token(p, .Colon, 1) - push_generic_token(p, .Eq, 0) - visit_expr(p, field.default_value) - } - - if i != len(list.list) - 1 { - push_generic_token(p, .Comma, 0) - } - } -} diff --git a/core/slice/slice.odin b/core/slice/slice.odin index 03791e7dd..3cab9189d 100644 --- a/core/slice/slice.odin +++ b/core/slice/slice.odin @@ -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 { diff --git a/core/sys/darwin/sync.odin b/core/sys/darwin/sync.odin index 361b4b8b4..121d3edef 100644 --- a/core/sys/darwin/sync.odin +++ b/core/sys/darwin/sync.odin @@ -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 --- } diff --git a/core/text/edit/text_edit.odin b/core/text/edit/text_edit.odin index 6f21c9860..a4f8c06b9 100644 --- a/core/text/edit/text_edit.odin +++ b/core/text/edit/text_edit.odin @@ -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' } diff --git a/core/text/i18n/gettext.odin b/core/text/i18n/gettext.odin index d5537a19c..3ac9109ef 100644 --- a/core/text/i18n/gettext.odin +++ b/core/text/i18n/gettext.odin @@ -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 EOT + 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 } diff --git a/core/time/iso8061.odin b/core/time/iso8061.odin new file mode 100644 index 000000000..528e0b00a --- /dev/null +++ b/core/time/iso8061.odin @@ -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 +} \ No newline at end of file diff --git a/core/time/rfc3339.odin b/core/time/rfc3339.odin index 30c255c79..0a2d431b7 100644 --- a/core/time/rfc3339.odin +++ b/core/time/rfc3339.odin @@ -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 '+', '-': diff --git a/core/time/time_wasi.odin b/core/time/time_wasi.odin index dacf911fc..3a5554d67 100644 --- a/core/time/time_wasi.odin +++ b/core/time/time_wasi.odin @@ -2,8 +2,6 @@ //+build wasi package time -import wasi "core:sys/wasm/wasi" - _IS_SUPPORTED :: false _now :: proc "contextless" () -> Time { diff --git a/examples/all/all_main.odin b/examples/all/all_main.odin index 3eebdf0b0..1077df1ae 100644 --- a/examples/all/all_main.odin +++ b/examples/all/all_main.odin @@ -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 diff --git a/src/bug_report.cpp b/src/bug_report.cpp index c73595e99..1f754ce7c 100644 --- a/src/bug_report.cpp +++ b/src/bug_report.cpp @@ -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"); } diff --git a/src/build_settings.cpp b/src/build_settings.cpp index f4e957479..d9454ba9b 100644 --- a/src/build_settings.cpp +++ b/src/build_settings.cpp @@ -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; - } } } diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 7e41c610a..eb18fce87 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -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 = {}; diff --git a/src/check_expr.cpp b/src/check_expr.cpp index d08f47a25..057e736b7 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -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; diff --git a/src/check_stmt.cpp b/src/check_stmt.cpp index 23a97696d..866cdb5a1 100644 --- a/src/check_stmt.cpp +++ b/src/check_stmt.cpp @@ -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; diff --git a/src/check_type.cpp b/src/check_type.cpp index 9381443fc..7ed657bee 100644 --- a/src/check_type.cpp +++ b/src/check_type.cpp @@ -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; } diff --git a/src/checker.cpp b/src/checker.cpp index 7cda0aa42..9d44c34dc 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -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"), diff --git a/src/checker_builtin_procs.hpp b/src/checker_builtin_procs.hpp index 04a8bb848..5f98bb7b3 100644 --- a/src/checker_builtin_procs.hpp +++ b/src/checker_builtin_procs.hpp @@ -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}, diff --git a/src/gb/gb.h b/src/gb/gb.h index 58fbfa071..17d5e97d1 100644 --- a/src/gb/gb.h +++ b/src/gb/gb.h @@ -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) diff --git a/src/llvm_backend_proc.cpp b/src/llvm_backend_proc.cpp index 736c54e52..3b9b1be05 100644 --- a/src/llvm_backend_proc.cpp +++ b/src/llvm_backend_proc.cpp @@ -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, "}"); diff --git a/src/llvm_backend_utility.cpp b/src/llvm_backend_utility.cpp index 5ebe0ddd9..f7674a8bc 100644 --- a/src/llvm_backend_utility.cpp +++ b/src/llvm_backend_utility.cpp @@ -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; diff --git a/src/parser.cpp b/src/parser.cpp index 682eec3de..d83bd4592 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -3499,8 +3499,14 @@ gb_internal Array 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) { diff --git a/tests/core/assets/I18N/mixed_context.mo b/tests/core/assets/I18N/mixed_context.mo new file mode 100644 index 000000000..53efee09a Binary files /dev/null and b/tests/core/assets/I18N/mixed_context.mo differ diff --git a/tests/core/assets/I18N/mixed_context.po b/tests/core/assets/I18N/mixed_context.po new file mode 100644 index 000000000..666e0a8d3 --- /dev/null +++ b/tests/core/assets/I18N/mixed_context.po @@ -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 \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" diff --git a/tests/core/assets/I18N/plur.mo b/tests/core/assets/I18N/plur.mo new file mode 100644 index 000000000..03243c0f6 Binary files /dev/null and b/tests/core/assets/I18N/plur.mo differ diff --git a/tests/core/assets/I18N/plur.po b/tests/core/assets/I18N/plur.po new file mode 100644 index 000000000..45c8209ca --- /dev/null +++ b/tests/core/assets/I18N/plur.po @@ -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 \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" diff --git a/tests/core/build.bat b/tests/core/build.bat index 5dd2d1932..7871e52e2 100644 --- a/tests/core/build.bat +++ b/tests/core/build.bat @@ -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 \ No newline at end of file +%PATH_TO_ODIN% run time %COMMON% %COLLECTION% -out:test_core_time.exe || exit /b \ No newline at end of file diff --git a/tests/core/container/test_core_avl.odin b/tests/core/container/test_core_avl.odin index f6343c5ea..2244ab7f6 100644 --- a/tests/core/container/test_core_avl.odin +++ b/tests/core/container/test_core_avl.odin @@ -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) } diff --git a/tests/core/container/test_core_container.odin b/tests/core/container/test_core_container.odin index f816a6bcb..7dd4a3628 100644 --- a/tests/core/container/test_core_container.odin +++ b/tests/core/container/test_core_container.odin @@ -20,7 +20,7 @@ main :: proc() { t := testing.T{} test_avl(&t) + test_rbtree(&t) test_small_array(&t) - tc.report(&t) } diff --git a/tests/core/container/test_core_rbtree.odin b/tests/core/container/test_core_rbtree.odin new file mode 100644 index 000000000..89742b1d0 --- /dev/null +++ b/tests/core/container/test_core_rbtree.odin @@ -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("") + return + } + if n.right != nil { + print_tree_node(n.right, indent + 1) + } + for _ in 0..", 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. \ No newline at end of file diff --git a/tests/core/odin/test_parser.odin b/tests/core/odin/test_parser.odin index 08f73a732..821b7a53c 100644 --- a/tests/core/odin/test_parser.odin +++ b/tests/core/odin/test_parser.odin @@ -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)) } diff --git a/tests/core/text/i18n/test_core_text_i18n.odin b/tests/core/text/i18n/test_core_text_i18n.odin index ec632d432..dcbdeb0c4 100644 --- a/tests/core/text/i18n/test_core_text_i18n.odin +++ b/tests/core/text/i18n/test_core_text_i18n.odin @@ -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) diff --git a/tests/core/time/test_core_time.odin b/tests/core/time/test_core_time.odin index 2cea47680..c6c6869a7 100644 --- a/tests/core/time/test_core_time.odin +++ b/tests/core/time/test_core_time.odin @@ -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 diff --git a/vendor/microui/microui.odin b/vendor/microui/microui.odin index 495289ede..cf39e2f55 100644 --- a/vendor/microui/microui.odin +++ b/vendor/microui/microui.odin @@ -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..= 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) diff --git a/vendor/wasm/js/runtime.js b/vendor/wasm/js/runtime.js index 320d74d68..8b4ad157b 100644 --- a/vendor/wasm/js/runtime.js +++ b/vendor/wasm/js/runtime.js @@ -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) {