mirror of
https://github.com/Ed94/Odin.git
synced 2026-06-17 19:32:23 -07:00
Merge remote-tracking branch 'offical/master'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -167,6 +167,9 @@ type_is_matrix :: proc($T: typeid) -> bool ---
|
||||
|
||||
type_has_nil :: proc($T: typeid) -> bool ---
|
||||
|
||||
type_is_matrix_row_major :: proc($T: typeid) -> bool where type_is_matrix(T) ---
|
||||
type_is_matrix_column_major :: proc($T: typeid) -> bool where type_is_matrix(T) ---
|
||||
|
||||
type_is_specialization_of :: proc($T, $S: typeid) -> bool ---
|
||||
|
||||
type_is_variant_of :: proc($U, $V: typeid) -> bool where type_is_union(U) ---
|
||||
|
||||
@@ -6,6 +6,9 @@ when ODIN_DEFAULT_TO_NIL_ALLOCATOR {
|
||||
} else when ODIN_DEFAULT_TO_PANIC_ALLOCATOR {
|
||||
default_allocator_proc :: panic_allocator_proc
|
||||
default_allocator :: panic_allocator
|
||||
} else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 {
|
||||
default_allocator :: default_wasm_allocator
|
||||
default_allocator_proc :: wasm_allocator_proc
|
||||
} else {
|
||||
default_allocator :: heap_allocator
|
||||
default_allocator_proc :: heap_allocator_proc
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package runtime
|
||||
|
||||
DEFAULT_TEMP_ALLOCATOR_BACKING_SIZE: int : #config(DEFAULT_TEMP_ALLOCATOR_BACKING_SIZE, 4 * Megabyte)
|
||||
NO_DEFAULT_TEMP_ALLOCATOR: bool : ODIN_OS == .Freestanding || ODIN_OS == .JS || ODIN_DEFAULT_TO_NIL_ALLOCATOR
|
||||
NO_DEFAULT_TEMP_ALLOCATOR: bool : ODIN_OS == .Freestanding || ODIN_DEFAULT_TO_NIL_ALLOCATOR
|
||||
|
||||
when NO_DEFAULT_TEMP_ALLOCATOR {
|
||||
Default_Temp_Allocator :: struct {}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,870 @@
|
||||
//+build wasm32, wasm64p32
|
||||
package runtime
|
||||
|
||||
import "base:intrinsics"
|
||||
|
||||
/*
|
||||
Port of emmalloc, modified for use in Odin.
|
||||
|
||||
Invariants:
|
||||
- Per-allocation header overhead is 8 bytes, smallest allocated payload
|
||||
amount is 8 bytes, and a multiple of 4 bytes.
|
||||
- Acquired memory blocks are subdivided into disjoint regions that lie
|
||||
next to each other.
|
||||
- A region is either in used or free.
|
||||
Used regions may be adjacent, and a used and unused region
|
||||
may be adjacent, but not two unused ones - they would be
|
||||
merged.
|
||||
- Memory allocation takes constant time, unless the alloc needs to wasm_memory_grow()
|
||||
or memory is very close to being exhausted.
|
||||
- Free and used regions are managed inside "root regions", which are slabs
|
||||
of memory acquired via wasm_memory_grow().
|
||||
- Memory retrieved using wasm_memory_grow() can not be given back to the OS.
|
||||
Therefore, frees are internal to the allocator.
|
||||
|
||||
Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
WASM_Allocator :: struct #no_copy {
|
||||
// The minimum alignment of allocations.
|
||||
alignment: uint,
|
||||
// A region that contains as payload a single forward linked list of pointers to
|
||||
// root regions of each disjoint region blocks.
|
||||
list_of_all_regions: ^Root_Region,
|
||||
// For each of the buckets, maintain a linked list head node. The head node for each
|
||||
// free region is a sentinel node that does not actually represent any free space, but
|
||||
// the sentinel is used to avoid awkward testing against (if node == freeRegionHeadNode)
|
||||
// when adding and removing elements from the linked list, i.e. we are guaranteed that
|
||||
// the sentinel node is always fixed and there, and the actual free region list elements
|
||||
// start at free_region_buckets[i].next each.
|
||||
free_region_buckets: [NUM_FREE_BUCKETS]Region,
|
||||
// A bitmask that tracks the population status for each of the 64 distinct memory regions:
|
||||
// a zero at bit position i means that the free list bucket i is empty. This bitmask is
|
||||
// used to avoid redundant scanning of the 64 different free region buckets: instead by
|
||||
// looking at the bitmask we can find in constant time an index to a free region bucket
|
||||
// that contains free memory of desired size.
|
||||
free_region_buckets_used: BUCKET_BITMASK_T,
|
||||
// Because wasm memory can only be allocated in pages of 64k at a time, we keep any
|
||||
// spilled/unused bytes that are left from the allocated pages here, first using this
|
||||
// when bytes are needed.
|
||||
spill: []byte,
|
||||
// Mutex for thread safety, only used if the target feature "atomics" is enabled.
|
||||
mu: Mutex_State,
|
||||
}
|
||||
|
||||
// Not required to be called, called on first allocation otherwise.
|
||||
wasm_allocator_init :: proc(a: ^WASM_Allocator, alignment: uint = 8) {
|
||||
assert(is_power_of_two(alignment), "alignment must be a power of two")
|
||||
assert(alignment > 4, "alignment must be more than 4")
|
||||
|
||||
a.alignment = alignment
|
||||
|
||||
for i in 0..<NUM_FREE_BUCKETS {
|
||||
a.free_region_buckets[i].next = &a.free_region_buckets[i]
|
||||
a.free_region_buckets[i].prev = a.free_region_buckets[i].next
|
||||
}
|
||||
|
||||
if !claim_more_memory(a, 3*size_of(Region)) {
|
||||
panic("wasm_allocator: initial memory could not be allocated")
|
||||
}
|
||||
}
|
||||
|
||||
global_default_wasm_allocator_data: WASM_Allocator
|
||||
|
||||
default_wasm_allocator :: proc() -> Allocator {
|
||||
return wasm_allocator(&global_default_wasm_allocator_data)
|
||||
}
|
||||
|
||||
wasm_allocator :: proc(a: ^WASM_Allocator) -> Allocator {
|
||||
return {
|
||||
data = a,
|
||||
procedure = wasm_allocator_proc,
|
||||
}
|
||||
}
|
||||
|
||||
wasm_allocator_proc :: proc(a: rawptr, mode: Allocator_Mode, size, alignment: int, old_memory: rawptr, old_size: int, loc := #caller_location) -> ([]byte, Allocator_Error) {
|
||||
a := (^WASM_Allocator)(a)
|
||||
if a == nil {
|
||||
a = &global_default_wasm_allocator_data
|
||||
}
|
||||
|
||||
if a.alignment == 0 {
|
||||
wasm_allocator_init(a)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case .Alloc:
|
||||
ptr := aligned_alloc(a, uint(alignment), uint(size), loc)
|
||||
if ptr == nil {
|
||||
return nil, .Out_Of_Memory
|
||||
}
|
||||
intrinsics.mem_zero(ptr, size)
|
||||
return ([^]byte)(ptr)[:size], nil
|
||||
|
||||
case .Alloc_Non_Zeroed:
|
||||
ptr := aligned_alloc(a, uint(alignment), uint(size), loc)
|
||||
if ptr == nil {
|
||||
return nil, .Out_Of_Memory
|
||||
}
|
||||
return ([^]byte)(ptr)[:size], nil
|
||||
|
||||
case .Resize:
|
||||
ptr := aligned_realloc(a, old_memory, uint(alignment), uint(size), loc)
|
||||
if ptr == nil {
|
||||
return nil, .Out_Of_Memory
|
||||
}
|
||||
|
||||
bytes := ([^]byte)(ptr)[:size]
|
||||
|
||||
if size > old_size {
|
||||
new_region := raw_data(bytes[old_size:])
|
||||
intrinsics.mem_zero(new_region, size - old_size)
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
|
||||
case .Resize_Non_Zeroed:
|
||||
ptr := aligned_realloc(a, old_memory, uint(alignment), uint(size), loc)
|
||||
if ptr == nil {
|
||||
return nil, .Out_Of_Memory
|
||||
}
|
||||
return ([^]byte)(ptr)[:size], nil
|
||||
|
||||
case .Free:
|
||||
free(a, old_memory, loc)
|
||||
return nil, nil
|
||||
|
||||
case .Free_All, .Query_Info:
|
||||
return nil, .Mode_Not_Implemented
|
||||
|
||||
case .Query_Features:
|
||||
set := (^Allocator_Mode_Set)(old_memory)
|
||||
if set != nil {
|
||||
set^ = {.Alloc, .Alloc_Non_Zeroed, .Free, .Resize, .Resize_Non_Zeroed, .Query_Features }
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
unreachable()
|
||||
}
|
||||
|
||||
// Returns the allocated size of the allocator (both free and used).
|
||||
// If `nil` is given, the global allocator is used.
|
||||
wasm_allocator_size :: proc(a: ^WASM_Allocator = nil) -> (size: uint) {
|
||||
a := a
|
||||
if a == nil {
|
||||
a = &global_default_wasm_allocator_data
|
||||
}
|
||||
|
||||
lock(a)
|
||||
defer unlock(a)
|
||||
|
||||
root := a.list_of_all_regions
|
||||
for root != nil {
|
||||
size += uint(uintptr(root.end_ptr) - uintptr(root))
|
||||
root = root.next
|
||||
}
|
||||
|
||||
size += len(a.spill)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Returns the amount of free memory on the allocator.
|
||||
// If `nil` is given, the global allocator is used.
|
||||
wasm_allocator_free_space :: proc(a: ^WASM_Allocator = nil) -> (free: uint) {
|
||||
a := a
|
||||
if a == nil {
|
||||
a = &global_default_wasm_allocator_data
|
||||
}
|
||||
|
||||
lock(a)
|
||||
defer unlock(a)
|
||||
|
||||
bucket_index: u64 = 0
|
||||
bucket_mask := a.free_region_buckets_used
|
||||
|
||||
for bucket_mask != 0 {
|
||||
index_add := intrinsics.count_trailing_zeros(bucket_mask)
|
||||
bucket_index += index_add
|
||||
bucket_mask >>= index_add
|
||||
for free_region := a.free_region_buckets[bucket_index].next; free_region != &a.free_region_buckets[bucket_index]; free_region = free_region.next {
|
||||
free += free_region.size - REGION_HEADER_SIZE
|
||||
}
|
||||
bucket_index += 1
|
||||
bucket_mask >>= 1
|
||||
}
|
||||
|
||||
free += len(a.spill)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
NUM_FREE_BUCKETS :: 64
|
||||
@(private="file")
|
||||
BUCKET_BITMASK_T :: u64
|
||||
|
||||
// Dynamic memory is subdivided into regions, in the format
|
||||
|
||||
// <size:u32> ..... <size:u32> | <size:u32> ..... <size:u32> | <size:u32> ..... <size:u32> | .....
|
||||
|
||||
// That is, at the bottom and top end of each memory region, the size of that region is stored. That allows traversing the
|
||||
// memory regions backwards and forwards. Because each allocation must be at least a multiple of 4 bytes, the lowest two bits of
|
||||
// each size field is unused. Free regions are distinguished by used regions by having the FREE_REGION_FLAG bit present
|
||||
// in the size field. I.e. for free regions, the size field is odd, and for used regions, the size field reads even.
|
||||
@(private="file")
|
||||
FREE_REGION_FLAG :: 0x1
|
||||
|
||||
// Attempts to alloc more than this many bytes would cause an overflow when calculating the size of a region,
|
||||
// therefore allocations larger than this are short-circuited immediately on entry.
|
||||
@(private="file")
|
||||
MAX_ALLOC_SIZE :: 0xFFFFFFC7
|
||||
|
||||
// A free region has the following structure:
|
||||
// <size:uint> <prevptr> <nextptr> ... <size:uint>
|
||||
|
||||
@(private="file")
|
||||
Region :: struct {
|
||||
size: uint,
|
||||
prev, next: ^Region,
|
||||
_at_the_end_of_this_struct_size: uint,
|
||||
}
|
||||
|
||||
// Each memory block starts with a Root_Region at the beginning.
|
||||
// The Root_Region specifies the size of the region block, and forms a linked
|
||||
// list of all Root_Regions in the program, starting with `list_of_all_regions`
|
||||
// below.
|
||||
@(private="file")
|
||||
Root_Region :: struct {
|
||||
size: u32,
|
||||
next: ^Root_Region,
|
||||
end_ptr: ^byte,
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
Mutex_State :: enum u32 {
|
||||
Unlocked = 0,
|
||||
Locked = 1,
|
||||
Waiting = 2,
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
lock :: proc(a: ^WASM_Allocator) {
|
||||
when intrinsics.has_target_feature("atomics") {
|
||||
@(cold)
|
||||
lock_slow :: proc(a: ^WASM_Allocator, curr_state: Mutex_State) {
|
||||
new_state := curr_state // Make a copy of it
|
||||
|
||||
spin_lock: for spin in 0..<i32(100) {
|
||||
state, ok := intrinsics.atomic_compare_exchange_weak_explicit(&a.mu, .Unlocked, new_state, .Acquire, .Consume)
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
|
||||
if state == .Waiting {
|
||||
break spin_lock
|
||||
}
|
||||
|
||||
for i := min(spin+1, 32); i > 0; i -= 1 {
|
||||
intrinsics.cpu_relax()
|
||||
}
|
||||
}
|
||||
|
||||
// Set just in case 100 iterations did not do it
|
||||
new_state = .Waiting
|
||||
|
||||
for {
|
||||
if intrinsics.atomic_exchange_explicit(&a.mu, .Waiting, .Acquire) == .Unlocked {
|
||||
return
|
||||
}
|
||||
|
||||
assert(intrinsics.wasm_memory_atomic_wait32((^u32)(&a.mu), u32(new_state), -1) != 0)
|
||||
intrinsics.cpu_relax()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if v := intrinsics.atomic_exchange_explicit(&a.mu, .Locked, .Acquire); v != .Unlocked {
|
||||
lock_slow(a, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
unlock :: proc(a: ^WASM_Allocator) {
|
||||
when intrinsics.has_target_feature("atomics") {
|
||||
@(cold)
|
||||
unlock_slow :: proc(a: ^WASM_Allocator) {
|
||||
for {
|
||||
s := intrinsics.wasm_memory_atomic_notify32((^u32)(&a.mu), 1)
|
||||
if s >= 1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch intrinsics.atomic_exchange_explicit(&a.mu, .Unlocked, .Release) {
|
||||
case .Unlocked:
|
||||
unreachable()
|
||||
case .Locked:
|
||||
// Okay
|
||||
case .Waiting:
|
||||
unlock_slow(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
assert_locked :: proc(a: ^WASM_Allocator) {
|
||||
when intrinsics.has_target_feature("atomics") {
|
||||
assert(intrinsics.atomic_load(&a.mu) != .Unlocked)
|
||||
}
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
has_alignment_uintptr :: proc(ptr: uintptr, #any_int alignment: uintptr) -> bool {
|
||||
return ptr & (alignment-1) == 0
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
has_alignment_uint :: proc(ptr: uint, alignment: uint) -> bool {
|
||||
return ptr & (alignment-1) == 0
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
has_alignment :: proc {
|
||||
has_alignment_uintptr,
|
||||
has_alignment_uint,
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
REGION_HEADER_SIZE :: 2*size_of(uint)
|
||||
|
||||
@(private="file")
|
||||
SMALLEST_ALLOCATION_SIZE :: 2*size_of(rawptr)
|
||||
|
||||
// Subdivide regions of free space into distinct circular doubly linked lists, where each linked list
|
||||
// represents a range of free space blocks. The following function compute_free_list_bucket() converts
|
||||
// an allocation size to the bucket index that should be looked at.
|
||||
#assert(NUM_FREE_BUCKETS == 64, "Following function is tailored specifically for the NUM_FREE_BUCKETS == 64 case")
|
||||
@(private="file")
|
||||
compute_free_list_bucket :: proc(size: uint) -> uint {
|
||||
if size < 128 { return (size >> 3) - 1 }
|
||||
|
||||
clz := intrinsics.count_leading_zeros(i32(size))
|
||||
bucket_index: i32 = ((clz > 19) \
|
||||
? 110 - (clz<<2) + ((i32)(size >> (u32)(29-clz)) ~ 4) \
|
||||
: min( 71 - (clz<<1) + ((i32)(size >> (u32)(30-clz)) ~ 2), NUM_FREE_BUCKETS-1))
|
||||
|
||||
assert(bucket_index >= 0)
|
||||
assert(bucket_index < NUM_FREE_BUCKETS)
|
||||
return uint(bucket_index)
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
prev_region :: proc(region: ^Region) -> ^Region {
|
||||
prev_region_size := ([^]uint)(region)[-1]
|
||||
prev_region_size = prev_region_size & ~uint(FREE_REGION_FLAG)
|
||||
return (^Region)(uintptr(region)-uintptr(prev_region_size))
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
next_region :: proc(region: ^Region) -> ^Region {
|
||||
return (^Region)(uintptr(region)+uintptr(region.size))
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
region_ceiling_size :: proc(region: ^Region) -> uint {
|
||||
return ([^]uint)(uintptr(region)+uintptr(region.size))[-1]
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
region_is_free :: proc(r: ^Region) -> bool {
|
||||
return region_ceiling_size(r) & FREE_REGION_FLAG >= 1
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
region_is_in_use :: proc(r: ^Region) -> bool {
|
||||
return r.size == region_ceiling_size(r)
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
region_payload_start_ptr :: proc(r: ^Region) -> [^]byte {
|
||||
return ([^]byte)(r)[size_of(uint):]
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
region_payload_end_ptr :: proc(r: ^Region) -> [^]byte {
|
||||
return ([^]byte)(r)[r.size-size_of(uint):]
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
create_used_region :: proc(ptr: rawptr, size: uint) {
|
||||
assert(has_alignment(uintptr(ptr), size_of(uint)))
|
||||
assert(has_alignment(size, size_of(uint)))
|
||||
assert(size >= size_of(Region))
|
||||
|
||||
uptr := ([^]uint)(ptr)
|
||||
uptr[0] = size
|
||||
uptr[size/size_of(uint)-1] = size
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
create_free_region :: proc(ptr: rawptr, size: uint) {
|
||||
assert(has_alignment(uintptr(ptr), size_of(uint)))
|
||||
assert(has_alignment(size, size_of(uint)))
|
||||
assert(size >= size_of(Region))
|
||||
|
||||
free_region := (^Region)(ptr)
|
||||
free_region.size = size
|
||||
([^]uint)(ptr)[size/size_of(uint)-1] = size | FREE_REGION_FLAG
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
prepend_to_free_list :: proc(region: ^Region, prepend_to: ^Region) {
|
||||
assert(region_is_free(region))
|
||||
region.next = prepend_to
|
||||
region.prev = prepend_to.prev
|
||||
prepend_to.prev = region
|
||||
region.prev.next = region
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
unlink_from_free_list :: proc(region: ^Region) {
|
||||
assert(region_is_free(region))
|
||||
region.prev.next = region.next
|
||||
region.next.prev = region.prev
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
link_to_free_list :: proc(a: ^WASM_Allocator, free_region: ^Region) {
|
||||
assert(free_region.size >= size_of(Region))
|
||||
bucket_index := compute_free_list_bucket(free_region.size-REGION_HEADER_SIZE)
|
||||
free_list_head := &a.free_region_buckets[bucket_index]
|
||||
free_region.prev = free_list_head
|
||||
free_region.next = free_list_head.next
|
||||
free_list_head.next = free_region
|
||||
free_region.next.prev = free_region
|
||||
a.free_region_buckets_used |= BUCKET_BITMASK_T(1) << bucket_index
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
claim_more_memory :: proc(a: ^WASM_Allocator, num_bytes: uint) -> bool {
|
||||
|
||||
PAGE_SIZE :: 64 * 1024
|
||||
|
||||
page_alloc :: proc(page_count: int) -> []byte {
|
||||
prev_page_count := intrinsics.wasm_memory_grow(0, uintptr(page_count))
|
||||
if prev_page_count < 0 { return nil }
|
||||
|
||||
ptr := ([^]byte)(uintptr(prev_page_count) * PAGE_SIZE)
|
||||
return ptr[:page_count * PAGE_SIZE]
|
||||
}
|
||||
|
||||
alloc :: proc(a: ^WASM_Allocator, num_bytes: uint) -> (bytes: [^]byte) #no_bounds_check {
|
||||
if uint(len(a.spill)) >= num_bytes {
|
||||
bytes = raw_data(a.spill[:num_bytes])
|
||||
a.spill = a.spill[num_bytes:]
|
||||
return
|
||||
}
|
||||
|
||||
pages := int((num_bytes / PAGE_SIZE) + 1)
|
||||
allocated := page_alloc(pages)
|
||||
if allocated == nil { return nil }
|
||||
|
||||
// If the allocated memory is a direct continuation of the spill from before,
|
||||
// we can just extend the spill.
|
||||
spill_end := uintptr(raw_data(a.spill)) + uintptr(len(a.spill))
|
||||
if spill_end == uintptr(raw_data(allocated)) {
|
||||
raw_spill := transmute(^Raw_Slice)(&a.spill)
|
||||
raw_spill.len += len(allocated)
|
||||
} else {
|
||||
// Otherwise, we have to "waste" the previous spill.
|
||||
// Now this is probably uncommon, and will only happen if another code path
|
||||
// is also requesting pages.
|
||||
a.spill = allocated
|
||||
}
|
||||
|
||||
bytes = raw_data(a.spill)
|
||||
a.spill = a.spill[num_bytes:]
|
||||
return
|
||||
}
|
||||
|
||||
num_bytes := num_bytes
|
||||
num_bytes = align_forward(num_bytes, a.alignment)
|
||||
|
||||
start_ptr := alloc(a, uint(num_bytes))
|
||||
if start_ptr == nil { return false }
|
||||
|
||||
assert(has_alignment(uintptr(start_ptr), align_of(uint)))
|
||||
end_ptr := start_ptr[num_bytes:]
|
||||
|
||||
end_sentinel_region := (^Region)(end_ptr[-size_of(Region):])
|
||||
create_used_region(end_sentinel_region, size_of(Region))
|
||||
|
||||
// If we are the sole user of wasm_memory_grow(), it will feed us continuous/consecutive memory addresses - take advantage
|
||||
// of that if so: instead of creating two disjoint memory regions blocks, expand the previous one to a larger size.
|
||||
prev_alloc_end_address := a.list_of_all_regions != nil ? a.list_of_all_regions.end_ptr : nil
|
||||
if start_ptr == prev_alloc_end_address {
|
||||
prev_end_sentinel := prev_region((^Region)(start_ptr))
|
||||
assert(region_is_in_use(prev_end_sentinel))
|
||||
prev_region := prev_region(prev_end_sentinel)
|
||||
|
||||
a.list_of_all_regions.end_ptr = end_ptr
|
||||
|
||||
// Two scenarios, either the last region of the previous block was in use, in which case we need to create
|
||||
// a new free region in the newly allocated space; or it was free, in which case we can extend that region
|
||||
// to cover a larger size.
|
||||
if region_is_free(prev_region) {
|
||||
new_free_region_size := uint(uintptr(end_sentinel_region) - uintptr(prev_region))
|
||||
unlink_from_free_list(prev_region)
|
||||
create_free_region(prev_region, new_free_region_size)
|
||||
link_to_free_list(a, prev_region)
|
||||
return true
|
||||
}
|
||||
|
||||
start_ptr = start_ptr[-size_of(Region):]
|
||||
} else {
|
||||
create_used_region(start_ptr, size_of(Region))
|
||||
|
||||
new_region_block := (^Root_Region)(start_ptr)
|
||||
new_region_block.next = a.list_of_all_regions
|
||||
new_region_block.end_ptr = end_ptr
|
||||
a.list_of_all_regions = new_region_block
|
||||
start_ptr = start_ptr[size_of(Region):]
|
||||
}
|
||||
|
||||
create_free_region(start_ptr, uint(uintptr(end_sentinel_region)-uintptr(start_ptr)))
|
||||
link_to_free_list(a, (^Region)(start_ptr))
|
||||
return true
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
validate_alloc_size :: proc(size: uint) -> uint {
|
||||
#assert(size_of(uint) >= size_of(uintptr))
|
||||
#assert(size_of(uint) % size_of(uintptr) == 0)
|
||||
|
||||
// NOTE: emmalloc aligns this forward on pointer size, but I think that is a mistake and will
|
||||
// do bad on wasm64p32.
|
||||
|
||||
validated_size := size > SMALLEST_ALLOCATION_SIZE ? align_forward(size, size_of(uint)) : SMALLEST_ALLOCATION_SIZE
|
||||
assert(validated_size >= size) // Assert we haven't wrapped.
|
||||
|
||||
return validated_size
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
allocate_memory :: proc(a: ^WASM_Allocator, alignment: uint, size: uint, loc := #caller_location) -> rawptr {
|
||||
|
||||
attempt_allocate :: proc(a: ^WASM_Allocator, free_region: ^Region, alignment, size: uint) -> rawptr {
|
||||
assert_locked(a)
|
||||
free_region := free_region
|
||||
|
||||
payload_start_ptr := uintptr(region_payload_start_ptr(free_region))
|
||||
payload_start_ptr_aligned := align_forward(payload_start_ptr, uintptr(alignment))
|
||||
payload_end_ptr := uintptr(region_payload_end_ptr(free_region))
|
||||
|
||||
if payload_start_ptr_aligned + uintptr(size) > payload_end_ptr {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We have enough free space, so the memory allocation will be made into this region. Remove this free region
|
||||
// from the list of free regions: whatever slop remains will be later added back to the free region pool.
|
||||
unlink_from_free_list(free_region)
|
||||
|
||||
// Before we proceed further, fix up the boundary between this and the preceding region,
|
||||
// so that the boundary between the two regions happens at a right spot for the payload to be aligned.
|
||||
if payload_start_ptr != payload_start_ptr_aligned {
|
||||
prev := prev_region(free_region)
|
||||
assert(region_is_in_use(prev))
|
||||
region_boundary_bump_amount := payload_start_ptr_aligned - payload_start_ptr
|
||||
new_this_region_size := free_region.size - uint(region_boundary_bump_amount)
|
||||
create_used_region(prev, prev.size + uint(region_boundary_bump_amount))
|
||||
free_region = (^Region)(uintptr(free_region) + region_boundary_bump_amount)
|
||||
free_region.size = new_this_region_size
|
||||
}
|
||||
|
||||
// Next, we need to decide whether this region is so large that it should be split into two regions,
|
||||
// one representing the newly used memory area, and at the high end a remaining leftover free area.
|
||||
// This splitting to two is done always if there is enough space for the high end to fit a region.
|
||||
// Carve 'size' bytes of payload off this region. So,
|
||||
// [sz prev next sz]
|
||||
// becomes
|
||||
// [sz payload sz] [sz prev next sz]
|
||||
if size_of(Region) + REGION_HEADER_SIZE + size <= free_region.size {
|
||||
new_free_region := (^Region)(uintptr(free_region) + REGION_HEADER_SIZE + uintptr(size))
|
||||
create_free_region(new_free_region, free_region.size - size - REGION_HEADER_SIZE)
|
||||
link_to_free_list(a, new_free_region)
|
||||
create_used_region(free_region, size + REGION_HEADER_SIZE)
|
||||
} else {
|
||||
// There is not enough space to split the free memory region into used+free parts, so consume the whole
|
||||
// region as used memory, not leaving a free memory region behind.
|
||||
// Initialize the free region as used by resetting the ceiling size to the same value as the size at bottom.
|
||||
([^]uint)(uintptr(free_region) + uintptr(free_region.size))[-1] = free_region.size
|
||||
}
|
||||
|
||||
return rawptr(uintptr(free_region) + size_of(uint))
|
||||
}
|
||||
|
||||
assert_locked(a)
|
||||
assert(is_power_of_two(alignment))
|
||||
assert(size <= MAX_ALLOC_SIZE, "allocation too big", loc=loc)
|
||||
|
||||
alignment := alignment
|
||||
alignment = max(alignment, a.alignment)
|
||||
|
||||
size := size
|
||||
size = validate_alloc_size(size)
|
||||
|
||||
// Attempt to allocate memory starting from smallest bucket that can contain the required amount of memory.
|
||||
// Under normal alignment conditions this should always be the first or second bucket we look at, but if
|
||||
// performing an allocation with complex alignment, we may need to look at multiple buckets.
|
||||
bucket_index := compute_free_list_bucket(size)
|
||||
bucket_mask := a.free_region_buckets_used >> bucket_index
|
||||
|
||||
// Loop through each bucket that has free regions in it, based on bits set in free_region_buckets_used bitmap.
|
||||
for bucket_mask != 0 {
|
||||
index_add := intrinsics.count_trailing_zeros(bucket_mask)
|
||||
bucket_index += uint(index_add)
|
||||
bucket_mask >>= index_add
|
||||
assert(bucket_index <= NUM_FREE_BUCKETS-1)
|
||||
assert(a.free_region_buckets_used & (BUCKET_BITMASK_T(1) << bucket_index) > 0)
|
||||
|
||||
free_region := a.free_region_buckets[bucket_index].next
|
||||
assert(free_region != nil)
|
||||
if free_region != &a.free_region_buckets[bucket_index] {
|
||||
ptr := attempt_allocate(a, free_region, alignment, size)
|
||||
if ptr != nil {
|
||||
return ptr
|
||||
}
|
||||
|
||||
// We were not able to allocate from the first region found in this bucket, so penalize
|
||||
// the region by cycling it to the end of the doubly circular linked list. (constant time)
|
||||
// This provides a randomized guarantee that when performing allocations of size k to a
|
||||
// bucket of [k-something, k+something] range, we will not always attempt to satisfy the
|
||||
// allocation from the same available region at the front of the list, but we try each
|
||||
// region in turn.
|
||||
unlink_from_free_list(free_region)
|
||||
prepend_to_free_list(free_region, &a.free_region_buckets[bucket_index])
|
||||
// But do not stick around to attempt to look at other regions in this bucket - move
|
||||
// to search the next populated bucket index if this did not fit. This gives a practical
|
||||
// "allocation in constant time" guarantee, since the next higher bucket will only have
|
||||
// regions that are all of strictly larger size than the requested allocation. Only if
|
||||
// there is a difficult alignment requirement we may fail to perform the allocation from
|
||||
// a region in the next bucket, and if so, we keep trying higher buckets until one of them
|
||||
// works.
|
||||
bucket_index += 1
|
||||
bucket_mask >>= 1
|
||||
} else {
|
||||
// This bucket was not populated after all with any regions,
|
||||
// but we just had a stale bit set to mark a populated bucket.
|
||||
// Reset the bit to update latest status so that we do not
|
||||
// redundantly look at this bucket again.
|
||||
a.free_region_buckets_used &= ~(BUCKET_BITMASK_T(1) << bucket_index)
|
||||
bucket_mask ~= 1
|
||||
}
|
||||
|
||||
assert((bucket_index == NUM_FREE_BUCKETS && bucket_mask == 0) || (bucket_mask == a.free_region_buckets_used >> bucket_index))
|
||||
}
|
||||
|
||||
// None of the buckets were able to accommodate an allocation. If this happens we are almost out of memory.
|
||||
// The largest bucket might contain some suitable regions, but we only looked at one region in that bucket, so
|
||||
// as a last resort, loop through more free regions in the bucket that represents the largest allocations available.
|
||||
// But only if the bucket representing largest allocations available is not any of the first thirty buckets,
|
||||
// these represent allocatable areas less than <1024 bytes - which could be a lot of scrap.
|
||||
// In such case, prefer to claim more memory right away.
|
||||
largest_bucket_index := NUM_FREE_BUCKETS - 1 - intrinsics.count_leading_zeros(a.free_region_buckets_used)
|
||||
// free_region will be null if there is absolutely no memory left. (all buckets are 100% used)
|
||||
free_region := a.free_region_buckets_used > 0 ? a.free_region_buckets[largest_bucket_index].next : nil
|
||||
// The 30 first free region buckets cover memory blocks < 2048 bytes, so skip looking at those here (too small)
|
||||
if a.free_region_buckets_used >> 30 > 0 {
|
||||
// Look only at a constant number of regions in this bucket max, to avoid bad worst case behavior.
|
||||
// If this many regions cannot find free space, we give up and prefer to claim more memory instead.
|
||||
max_regions_to_try_before_giving_up :: 99
|
||||
num_tries_left := max_regions_to_try_before_giving_up
|
||||
for ; free_region != &a.free_region_buckets[largest_bucket_index] && num_tries_left > 0; num_tries_left -= 1 {
|
||||
ptr := attempt_allocate(a, free_region, alignment, size)
|
||||
if ptr != nil {
|
||||
return ptr
|
||||
}
|
||||
free_region = free_region.next
|
||||
}
|
||||
}
|
||||
|
||||
// We were unable to find a free memory region. Must claim more memory!
|
||||
num_bytes_to_claim := size+size_of(Region)*3
|
||||
if alignment > a.alignment {
|
||||
num_bytes_to_claim += alignment
|
||||
}
|
||||
success := claim_more_memory(a, num_bytes_to_claim)
|
||||
if (success) {
|
||||
// Try allocate again with the newly available memory.
|
||||
return allocate_memory(a, alignment, size)
|
||||
}
|
||||
|
||||
// also claim_more_memory failed, we are really really constrained :( As a last resort, go back to looking at the
|
||||
// bucket we already looked at above, continuing where the above search left off - perhaps there are
|
||||
// regions we overlooked the first time that might be able to satisfy the allocation.
|
||||
if free_region != nil {
|
||||
for free_region != &a.free_region_buckets[largest_bucket_index] {
|
||||
ptr := attempt_allocate(a, free_region, alignment, size)
|
||||
if ptr != nil {
|
||||
return ptr
|
||||
}
|
||||
free_region = free_region.next
|
||||
}
|
||||
}
|
||||
|
||||
// Fully out of memory.
|
||||
return nil
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
aligned_alloc :: proc(a: ^WASM_Allocator, alignment, size: uint, loc := #caller_location) -> rawptr {
|
||||
lock(a)
|
||||
defer unlock(a)
|
||||
|
||||
return allocate_memory(a, alignment, size, loc)
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
free :: proc(a: ^WASM_Allocator, ptr: rawptr, loc := #caller_location) {
|
||||
if ptr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
region_start_ptr := uintptr(ptr) - size_of(uint)
|
||||
region := (^Region)(region_start_ptr)
|
||||
assert(has_alignment(region_start_ptr, size_of(uint)))
|
||||
|
||||
lock(a)
|
||||
defer unlock(a)
|
||||
|
||||
size := region.size
|
||||
assert(region_is_in_use(region), "double free", loc=loc)
|
||||
|
||||
prev_region_size_field := ([^]uint)(region)[-1]
|
||||
prev_region_size := prev_region_size_field & ~uint(FREE_REGION_FLAG)
|
||||
if prev_region_size_field != prev_region_size {
|
||||
prev_region := (^Region)(uintptr(region) - uintptr(prev_region_size))
|
||||
unlink_from_free_list(prev_region)
|
||||
region_start_ptr = uintptr(prev_region)
|
||||
size += prev_region_size
|
||||
}
|
||||
|
||||
next_reg := next_region(region)
|
||||
size_at_end := (^uint)(region_payload_end_ptr(next_reg))^
|
||||
if next_reg.size != size_at_end {
|
||||
unlink_from_free_list(next_reg)
|
||||
size += next_reg.size
|
||||
}
|
||||
|
||||
create_free_region(rawptr(region_start_ptr), size)
|
||||
link_to_free_list(a, (^Region)(region_start_ptr))
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
aligned_realloc :: proc(a: ^WASM_Allocator, ptr: rawptr, alignment, size: uint, loc := #caller_location) -> rawptr {
|
||||
|
||||
attempt_region_resize :: proc(a: ^WASM_Allocator, region: ^Region, size: uint) -> bool {
|
||||
lock(a)
|
||||
defer unlock(a)
|
||||
|
||||
// First attempt to resize this region, if the next region that follows this one
|
||||
// is a free region.
|
||||
next_reg := next_region(region)
|
||||
next_region_end_ptr := uintptr(next_reg) + uintptr(next_reg.size)
|
||||
size_at_ceiling := ([^]uint)(next_region_end_ptr)[-1]
|
||||
if next_reg.size != size_at_ceiling { // Next region is free?
|
||||
assert(region_is_free(next_reg))
|
||||
new_next_region_start_ptr := uintptr(region) + uintptr(size)
|
||||
assert(has_alignment(new_next_region_start_ptr, size_of(uint)))
|
||||
// Next region does not shrink to too small size?
|
||||
if new_next_region_start_ptr + size_of(Region) <= next_region_end_ptr {
|
||||
unlink_from_free_list(next_reg)
|
||||
create_free_region(rawptr(new_next_region_start_ptr), uint(next_region_end_ptr - new_next_region_start_ptr))
|
||||
link_to_free_list(a, (^Region)(new_next_region_start_ptr))
|
||||
create_used_region(region, uint(new_next_region_start_ptr - uintptr(region)))
|
||||
return true
|
||||
}
|
||||
// If we remove the next region altogether, allocation is satisfied?
|
||||
if new_next_region_start_ptr <= next_region_end_ptr {
|
||||
unlink_from_free_list(next_reg)
|
||||
create_used_region(region, region.size + next_reg.size)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// Next region is an used region - we cannot change its starting address. However if we are shrinking the
|
||||
// size of this region, we can create a new free region between this and the next used region.
|
||||
if size + size_of(Region) <= region.size {
|
||||
free_region_size := region.size - size
|
||||
create_used_region(region, size)
|
||||
free_region := (^Region)(uintptr(region) + uintptr(size))
|
||||
create_free_region(free_region, free_region_size)
|
||||
link_to_free_list(a, free_region)
|
||||
return true
|
||||
} else if size <= region.size {
|
||||
// Caller was asking to shrink the size, but due to not being able to fit a full Region in the shrunk
|
||||
// area, we cannot actually do anything. This occurs if the shrink amount is really small. In such case,
|
||||
// just call it success without doing any work.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if ptr == nil {
|
||||
return aligned_alloc(a, alignment, size, loc)
|
||||
}
|
||||
|
||||
if size == 0 {
|
||||
free(a, ptr, loc)
|
||||
return nil
|
||||
}
|
||||
|
||||
if size > MAX_ALLOC_SIZE {
|
||||
return nil
|
||||
}
|
||||
|
||||
assert(is_power_of_two(alignment))
|
||||
assert(has_alignment(uintptr(ptr), alignment), "realloc on different alignment than original allocation", loc=loc)
|
||||
|
||||
size := size
|
||||
size = validate_alloc_size(size)
|
||||
|
||||
region := (^Region)(uintptr(ptr) - size_of(uint))
|
||||
|
||||
// Attempt an in-place resize.
|
||||
if attempt_region_resize(a, region, size + REGION_HEADER_SIZE) {
|
||||
return ptr
|
||||
}
|
||||
|
||||
// Can't do it in-place, allocate new region and copy over.
|
||||
newptr := aligned_alloc(a, alignment, size, loc)
|
||||
if newptr != nil {
|
||||
intrinsics.mem_copy(newptr, ptr, min(size, region.size - REGION_HEADER_SIZE))
|
||||
free(a, ptr, loc=loc)
|
||||
}
|
||||
|
||||
return newptr
|
||||
}
|
||||
+10
-1
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
@@ -539,8 +539,6 @@ marshal_to_writer :: proc(w: io.Writer, v: any, opt: ^Marshal_Options) -> (err:
|
||||
case: panic("unknown bit_size size")
|
||||
}
|
||||
io.write_u64(w, bit_data) or_return
|
||||
|
||||
return .Unsupported_Type
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
+148
-54
@@ -13,20 +13,7 @@ import "core:unicode/utf8"
|
||||
|
||||
// Internal data structure that stores the required information for formatted printing
|
||||
Info :: struct {
|
||||
minus: bool,
|
||||
plus: bool,
|
||||
space: bool,
|
||||
zero: bool,
|
||||
hash: bool,
|
||||
width_set: bool,
|
||||
prec_set: bool,
|
||||
|
||||
width: int,
|
||||
prec: int,
|
||||
indent: int,
|
||||
|
||||
ignore_user_formatters: bool,
|
||||
in_bad: bool,
|
||||
using state: Info_State,
|
||||
|
||||
writer: io.Writer,
|
||||
arg: any, // Temporary
|
||||
@@ -39,6 +26,24 @@ Info :: struct {
|
||||
n: int, // bytes written
|
||||
}
|
||||
|
||||
Info_State :: struct {
|
||||
minus: bool,
|
||||
plus: bool,
|
||||
space: bool,
|
||||
zero: bool,
|
||||
hash: bool,
|
||||
width_set: bool,
|
||||
prec_set: bool,
|
||||
|
||||
ignore_user_formatters: bool,
|
||||
in_bad: bool,
|
||||
|
||||
width: int,
|
||||
prec: int,
|
||||
indent: int,
|
||||
}
|
||||
|
||||
|
||||
// Custom formatter signature. It returns true if the formatting was successful and false when it could not be done
|
||||
User_Formatter :: #type proc(fi: ^Info, arg: any, verb: rune) -> bool
|
||||
|
||||
@@ -994,6 +999,33 @@ _fmt_int :: proc(fi: ^Info, u: u64, base: int, is_signed: bool, bit_size: int, d
|
||||
}
|
||||
}
|
||||
|
||||
buf: [256]byte
|
||||
start := 0
|
||||
|
||||
if fi.hash && !is_signed {
|
||||
switch base {
|
||||
case 2:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'b', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 8:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'o', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 12:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'o', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 16:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'x', &fi.n)
|
||||
start = 2
|
||||
}
|
||||
}
|
||||
|
||||
prec := 0
|
||||
if fi.prec_set {
|
||||
prec = fi.prec
|
||||
@@ -1019,14 +1051,10 @@ _fmt_int :: proc(fi: ^Info, u: u64, base: int, is_signed: bool, bit_size: int, d
|
||||
panic("_fmt_int: unknown base, whoops")
|
||||
}
|
||||
|
||||
buf: [256]byte
|
||||
start := 0
|
||||
|
||||
flags: strconv.Int_Flags
|
||||
if fi.hash { flags |= {.Prefix} }
|
||||
if fi.plus { flags |= {.Plus} }
|
||||
if fi.hash && !fi.zero && start == 0 { flags |= {.Prefix} }
|
||||
if fi.plus { flags |= {.Plus} }
|
||||
s := strconv.append_bits(buf[start:], u, base, is_signed, bit_size, digits, flags)
|
||||
|
||||
prev_zero := fi.zero
|
||||
defer fi.zero = prev_zero
|
||||
fi.zero = false
|
||||
@@ -1056,6 +1084,33 @@ _fmt_int_128 :: proc(fi: ^Info, u: u128, base: int, is_signed: bool, bit_size: i
|
||||
}
|
||||
}
|
||||
|
||||
buf: [256]byte
|
||||
start := 0
|
||||
|
||||
if fi.hash && !is_signed {
|
||||
switch base {
|
||||
case 2:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'b', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 8:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'o', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 12:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'o', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 16:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'x', &fi.n)
|
||||
start = 2
|
||||
}
|
||||
}
|
||||
|
||||
prec := 0
|
||||
if fi.prec_set {
|
||||
prec = fi.prec
|
||||
@@ -1081,12 +1136,9 @@ _fmt_int_128 :: proc(fi: ^Info, u: u128, base: int, is_signed: bool, bit_size: i
|
||||
panic("_fmt_int: unknown base, whoops")
|
||||
}
|
||||
|
||||
buf: [256]byte
|
||||
start := 0
|
||||
|
||||
flags: strconv.Int_Flags
|
||||
if fi.hash && !fi.zero { flags |= {.Prefix} }
|
||||
if fi.plus { flags |= {.Plus} }
|
||||
if fi.hash && !fi.zero && start == 0 { flags |= {.Prefix} }
|
||||
if fi.plus { flags |= {.Plus} }
|
||||
s := strconv.append_bits_128(buf[start:], u, base, is_signed, bit_size, digits, flags)
|
||||
|
||||
if fi.hash && fi.zero && fi.indent == 0 {
|
||||
@@ -1777,7 +1829,7 @@ fmt_write_array :: proc(fi: ^Info, array_data: rawptr, count: int, elem_size: in
|
||||
// Returns: A boolean value indicating whether to continue processing the tag
|
||||
//
|
||||
@(private)
|
||||
handle_tag :: proc(data: rawptr, info: reflect.Type_Info_Struct, idx: int, verb: ^rune, optional_len: ^int, use_nul_termination: ^bool) -> (do_continue: bool) {
|
||||
handle_tag :: proc(state: ^Info_State, data: rawptr, info: reflect.Type_Info_Struct, idx: int, verb: ^rune, optional_len: ^int, use_nul_termination: ^bool) -> (do_continue: bool) {
|
||||
handle_optional_len :: proc(data: rawptr, info: reflect.Type_Info_Struct, field_name: string, optional_len: ^int) {
|
||||
if optional_len == nil {
|
||||
return
|
||||
@@ -1794,45 +1846,83 @@ handle_tag :: proc(data: rawptr, info: reflect.Type_Info_Struct, idx: int, verb:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tag := info.tags[idx]
|
||||
if vt, ok := reflect.struct_tag_lookup(reflect.Struct_Tag(tag), "fmt"); ok {
|
||||
value := strings.trim_space(string(vt))
|
||||
switch value {
|
||||
case "": return false
|
||||
case "": return false
|
||||
case "-": return true
|
||||
}
|
||||
r, w := utf8.decode_rune_in_string(value)
|
||||
value = value[w:]
|
||||
if value == "" || value[0] == ',' {
|
||||
if verb^ == 'w' {
|
||||
// TODO(bill): is this a good idea overriding that field tags if 'w' is used?
|
||||
switch r {
|
||||
case 's': r = 'q'
|
||||
case: r = 'w'
|
||||
}
|
||||
|
||||
fi := state
|
||||
|
||||
head, _, tail := strings.partition(value, ",")
|
||||
|
||||
i := 0
|
||||
prefix_loop: for ; i < len(head); i += 1 {
|
||||
switch head[i] {
|
||||
case '+':
|
||||
fi.plus = true
|
||||
case '-':
|
||||
fi.minus = true
|
||||
fi.zero = false
|
||||
case ' ':
|
||||
fi.space = true
|
||||
case '#':
|
||||
fi.hash = true
|
||||
case '0':
|
||||
fi.zero = !fi.minus
|
||||
case:
|
||||
break prefix_loop
|
||||
}
|
||||
verb^ = r
|
||||
if len(value) > 0 && value[0] == ',' {
|
||||
field_name := value[1:]
|
||||
if field_name == "0" {
|
||||
if use_nul_termination != nil {
|
||||
use_nul_termination^ = true
|
||||
}
|
||||
} else {
|
||||
switch r {
|
||||
case 's', 'q':
|
||||
}
|
||||
|
||||
fi.width, i, fi.width_set = _parse_int(head, i)
|
||||
if i < len(head) && head[i] == '.' {
|
||||
i += 1
|
||||
prev_i := i
|
||||
fi.prec, i, fi.prec_set = _parse_int(head, i)
|
||||
if i == prev_i {
|
||||
fi.prec = 0
|
||||
fi.prec_set = true
|
||||
}
|
||||
}
|
||||
|
||||
r: rune
|
||||
if i >= len(head) || head[i] == ' ' {
|
||||
r = 'v'
|
||||
} else {
|
||||
r, _ = utf8.decode_rune_in_string(head[i:])
|
||||
}
|
||||
if verb^ == 'w' {
|
||||
// TODO(bill): is this a good idea overriding that field tags if 'w' is used?
|
||||
switch r {
|
||||
case 's': r = 'q'
|
||||
case: r = 'w'
|
||||
}
|
||||
}
|
||||
verb^ = r
|
||||
if tail != "" {
|
||||
field_name := tail
|
||||
if field_name == "0" {
|
||||
if use_nul_termination != nil {
|
||||
use_nul_termination^ = true
|
||||
}
|
||||
} else {
|
||||
switch r {
|
||||
case 's', 'q':
|
||||
handle_optional_len(data, info, field_name, optional_len)
|
||||
case 'v', 'w':
|
||||
#partial switch reflect.type_kind(info.types[idx].id) {
|
||||
case .String, .Multi_Pointer, .Array, .Slice, .Dynamic_Array:
|
||||
handle_optional_len(data, info, field_name, optional_len)
|
||||
case 'v', 'w':
|
||||
#partial switch reflect.type_kind(info.types[idx].id) {
|
||||
case .String, .Multi_Pointer, .Array, .Slice, .Dynamic_Array:
|
||||
handle_optional_len(data, info, field_name, optional_len)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return
|
||||
}
|
||||
// Formats a struct for output, handling various struct types (e.g., SOA, raw unions)
|
||||
//
|
||||
@@ -1980,7 +2070,9 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
|
||||
optional_len: int = -1
|
||||
use_nul_termination: bool = false
|
||||
verb := the_verb if the_verb == 'w' else 'v'
|
||||
if handle_tag(v.data, info, i, &verb, &optional_len, &use_nul_termination) {
|
||||
|
||||
new_state := fi.state
|
||||
if handle_tag(&new_state, v.data, info, i, &verb, &optional_len, &use_nul_termination) {
|
||||
continue
|
||||
}
|
||||
field_count += 1
|
||||
@@ -2005,8 +2097,11 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
|
||||
if t := info.types[i]; reflect.is_any(t) {
|
||||
io.write_string(fi.writer, "any{}", &fi.n)
|
||||
} else {
|
||||
prev_state := fi.state
|
||||
fi.state = new_state
|
||||
data := rawptr(uintptr(v.data) + info.offsets[i])
|
||||
fmt_arg(fi, any{data, t.id}, verb)
|
||||
fi.state = prev_state
|
||||
}
|
||||
|
||||
if do_trailing_comma { io.write_string(fi.writer, ",\n", &fi.n) }
|
||||
@@ -2679,7 +2774,6 @@ fmt_value :: proc(fi: ^Info, v: any, verb: rune) {
|
||||
io.write_byte(fi.writer, '[' if verb != 'w' else '{', &fi.n)
|
||||
io.write_byte(fi.writer, '\n', &fi.n)
|
||||
defer {
|
||||
io.write_byte(fi.writer, '\n', &fi.n)
|
||||
fmt_write_indent(fi)
|
||||
io.write_byte(fi.writer, ']' if verb != 'w' else '}', &fi.n)
|
||||
}
|
||||
|
||||
+377
-380
@@ -1,381 +1,378 @@
|
||||
/*
|
||||
Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
|
||||
Made available under Odin's BSD-3 license.
|
||||
|
||||
List of contributors:
|
||||
Jeroen van Rijn: Initial implementation.
|
||||
*/
|
||||
|
||||
|
||||
// package qoi implements a QOI image reader
|
||||
//
|
||||
// The QOI specification is at https://qoiformat.org.
|
||||
package qoi
|
||||
|
||||
import "core:image"
|
||||
import "core:compress"
|
||||
import "core:bytes"
|
||||
|
||||
Error :: image.Error
|
||||
Image :: image.Image
|
||||
Options :: image.Options
|
||||
|
||||
RGB_Pixel :: image.RGB_Pixel
|
||||
RGBA_Pixel :: image.RGBA_Pixel
|
||||
|
||||
save_to_buffer :: proc(output: ^bytes.Buffer, img: ^Image, options := Options{}, allocator := context.allocator) -> (err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
if img == nil {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
if output == nil {
|
||||
return .Invalid_Output
|
||||
}
|
||||
|
||||
pixels := img.width * img.height
|
||||
if pixels == 0 || pixels > image.MAX_DIMENSIONS {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
// QOI supports only 8-bit images with 3 or 4 channels.
|
||||
if img.depth != 8 || img.channels < 3 || img.channels > 4 {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
if img.channels * pixels != len(img.pixels.buf) {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
written := 0
|
||||
|
||||
// Calculate and allocate maximum size. We'll reclaim space to actually written output at the end.
|
||||
max_size := pixels * (img.channels + 1) + size_of(image.QOI_Header) + size_of(u64be)
|
||||
|
||||
if resize(&output.buf, max_size) != nil {
|
||||
return .Unable_To_Allocate_Or_Resize
|
||||
}
|
||||
|
||||
header := image.QOI_Header{
|
||||
magic = image.QOI_Magic,
|
||||
width = u32be(img.width),
|
||||
height = u32be(img.height),
|
||||
channels = u8(img.channels),
|
||||
color_space = .Linear if .qoi_all_channels_linear in options else .sRGB,
|
||||
}
|
||||
header_bytes := transmute([size_of(image.QOI_Header)]u8)header
|
||||
|
||||
copy(output.buf[written:], header_bytes[:])
|
||||
written += size_of(image.QOI_Header)
|
||||
|
||||
/*
|
||||
Encode loop starts here.
|
||||
*/
|
||||
seen: [64]RGBA_Pixel
|
||||
pix := RGBA_Pixel{0, 0, 0, 255}
|
||||
prev := pix
|
||||
|
||||
seen[qoi_hash(pix)] = pix
|
||||
|
||||
input := img.pixels.buf[:]
|
||||
run := u8(0)
|
||||
|
||||
for len(input) > 0 {
|
||||
if img.channels == 4 {
|
||||
pix = (^RGBA_Pixel)(raw_data(input))^
|
||||
} else {
|
||||
pix.rgb = (^RGB_Pixel)(raw_data(input))^
|
||||
}
|
||||
input = input[img.channels:]
|
||||
|
||||
if pix == prev {
|
||||
run += 1
|
||||
// As long as the pixel matches the last one, accumulate the run total.
|
||||
// If we reach the max run length or the end of the image, write the run.
|
||||
if run == 62 || len(input) == 0 {
|
||||
// Encode and write run
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
|
||||
written += 1
|
||||
run = 0
|
||||
}
|
||||
} else {
|
||||
if run > 0 {
|
||||
// The pixel differs from the previous one, but we still need to write the pending run.
|
||||
// Encode and write run
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
|
||||
written += 1
|
||||
run = 0
|
||||
}
|
||||
|
||||
index := qoi_hash(pix)
|
||||
|
||||
if seen[index] == pix {
|
||||
// Write indexed pixel
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.INDEX) | index
|
||||
written += 1
|
||||
} else {
|
||||
// Add pixel to index
|
||||
seen[index] = pix
|
||||
|
||||
// If the alpha matches the previous pixel's alpha, we don't need to write a full RGBA literal.
|
||||
if pix.a == prev.a {
|
||||
// Delta
|
||||
d := pix.rgb - prev.rgb
|
||||
|
||||
// DIFF, biased and modulo 256
|
||||
_d := d + 2
|
||||
|
||||
// LUMA, biased and modulo 256
|
||||
_l := RGB_Pixel{ d.r - d.g + 8, d.g + 32, d.b - d.g + 8 }
|
||||
|
||||
if _d.r < 4 && _d.g < 4 && _d.b < 4 {
|
||||
// Delta is between -2 and 1 inclusive
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.DIFF) | _d.r << 4 | _d.g << 2 | _d.b
|
||||
written += 1
|
||||
} else if _l.r < 16 && _l.g < 64 && _l.b < 16 {
|
||||
// Biased luma is between {-8..7, -32..31, -8..7}
|
||||
output.buf[written ] = u8(QOI_Opcode_Tag.LUMA) | _l.g
|
||||
output.buf[written + 1] = _l.r << 4 | _l.b
|
||||
written += 2
|
||||
} else {
|
||||
// Write RGB literal
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RGB)
|
||||
pix_bytes := transmute([4]u8)pix
|
||||
copy(output.buf[written + 1:], pix_bytes[:3])
|
||||
written += 4
|
||||
}
|
||||
} else {
|
||||
// Write RGBA literal
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RGBA)
|
||||
pix_bytes := transmute([4]u8)pix
|
||||
copy(output.buf[written + 1:], pix_bytes[:])
|
||||
written += 5
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = pix
|
||||
}
|
||||
|
||||
trailer := []u8{0, 0, 0, 0, 0, 0, 0, 1}
|
||||
copy(output.buf[written:], trailer[:])
|
||||
written += len(trailer)
|
||||
|
||||
resize(&output.buf, written)
|
||||
return nil
|
||||
}
|
||||
|
||||
load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
|
||||
ctx := &compress.Context_Memory_Input{
|
||||
input_data = data,
|
||||
}
|
||||
|
||||
img, err = load_from_context(ctx, options, allocator)
|
||||
return img, err
|
||||
}
|
||||
|
||||
@(optimization_mode="speed")
|
||||
load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
|
||||
context.allocator = allocator
|
||||
options := options
|
||||
|
||||
if .info in options {
|
||||
options |= {.return_metadata, .do_not_decompress_image}
|
||||
options -= {.info}
|
||||
}
|
||||
|
||||
if .return_header in options && .return_metadata in options {
|
||||
options -= {.return_header}
|
||||
}
|
||||
|
||||
header := image.read_data(ctx, image.QOI_Header) or_return
|
||||
if header.magic != image.QOI_Magic {
|
||||
return img, .Invalid_Signature
|
||||
}
|
||||
|
||||
if img == nil {
|
||||
img = new(Image)
|
||||
}
|
||||
img.which = .QOI
|
||||
|
||||
if .return_metadata in options {
|
||||
info := new(image.QOI_Info)
|
||||
info.header = header
|
||||
img.metadata = info
|
||||
}
|
||||
|
||||
if header.channels != 3 && header.channels != 4 {
|
||||
return img, .Invalid_Number_Of_Channels
|
||||
}
|
||||
|
||||
if header.color_space != .sRGB && header.color_space != .Linear {
|
||||
return img, .Invalid_Color_Space
|
||||
}
|
||||
|
||||
if header.width == 0 || header.height == 0 {
|
||||
return img, .Invalid_Image_Dimensions
|
||||
}
|
||||
|
||||
total_pixels := header.width * header.height
|
||||
if total_pixels > image.MAX_DIMENSIONS {
|
||||
return img, .Image_Dimensions_Too_Large
|
||||
}
|
||||
|
||||
img.width = int(header.width)
|
||||
img.height = int(header.height)
|
||||
img.channels = 4 if .alpha_add_if_missing in options else int(header.channels)
|
||||
img.depth = 8
|
||||
|
||||
if .do_not_decompress_image in options {
|
||||
img.channels = int(header.channels)
|
||||
return
|
||||
}
|
||||
|
||||
bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), img.channels, 8)
|
||||
|
||||
if resize(&img.pixels.buf, bytes_needed) != nil {
|
||||
return img, .Unable_To_Allocate_Or_Resize
|
||||
}
|
||||
|
||||
/*
|
||||
Decode loop starts here.
|
||||
*/
|
||||
seen: [64]RGBA_Pixel
|
||||
pix := RGBA_Pixel{0, 0, 0, 255}
|
||||
seen[qoi_hash(pix)] = pix
|
||||
pixels := img.pixels.buf[:]
|
||||
|
||||
decode: for len(pixels) > 0 {
|
||||
data := image.read_u8(ctx) or_return
|
||||
|
||||
tag := QOI_Opcode_Tag(data)
|
||||
#partial switch tag {
|
||||
case .RGB:
|
||||
pix.rgb = image.read_data(ctx, RGB_Pixel) or_return
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .RGBA:
|
||||
pix = image.read_data(ctx, RGBA_Pixel) or_return
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case:
|
||||
// 2-bit tag
|
||||
tag = QOI_Opcode_Tag(data & QOI_Opcode_Mask)
|
||||
#partial switch tag {
|
||||
case .INDEX:
|
||||
pix = seen[data & 63]
|
||||
|
||||
case .DIFF:
|
||||
diff_r := ((data >> 4) & 3) - 2
|
||||
diff_g := ((data >> 2) & 3) - 2
|
||||
diff_b := ((data >> 0) & 3) - 2
|
||||
|
||||
pix += {diff_r, diff_g, diff_b, 0}
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .LUMA:
|
||||
data2 := image.read_u8(ctx) or_return
|
||||
|
||||
diff_g := (data & 63) - 32
|
||||
diff_r := diff_g - 8 + ((data2 >> 4) & 15)
|
||||
diff_b := diff_g - 8 + (data2 & 15)
|
||||
|
||||
pix += {diff_r, diff_g, diff_b, 0}
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .RUN:
|
||||
if length := int(data & 63) + 1; (length * img.channels) > len(pixels) {
|
||||
return img, .Corrupt
|
||||
} else {
|
||||
#no_bounds_check for _ in 0..<length {
|
||||
copy(pixels, pix[:img.channels])
|
||||
pixels = pixels[img.channels:]
|
||||
}
|
||||
}
|
||||
|
||||
continue decode
|
||||
|
||||
case:
|
||||
unreachable()
|
||||
}
|
||||
}
|
||||
|
||||
#no_bounds_check {
|
||||
copy(pixels, pix[:img.channels])
|
||||
pixels = pixels[img.channels:]
|
||||
}
|
||||
}
|
||||
|
||||
// The byte stream's end is marked with 7 0x00 bytes followed by a single 0x01 byte.
|
||||
trailer, trailer_err := compress.read_data(ctx, u64be)
|
||||
if trailer_err != nil || trailer != 0x1 {
|
||||
return img, .Missing_Or_Corrupt_Trailer
|
||||
}
|
||||
|
||||
if .alpha_premultiply in options && !image.alpha_drop_if_present(img, options) {
|
||||
return img, .Post_Processing_Error
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Cleanup of image-specific data.
|
||||
*/
|
||||
destroy :: proc(img: ^Image) {
|
||||
if img == nil {
|
||||
/*
|
||||
Nothing to do.
|
||||
Load must've returned with an error.
|
||||
*/
|
||||
return
|
||||
}
|
||||
|
||||
bytes.buffer_destroy(&img.pixels)
|
||||
|
||||
if v, ok := img.metadata.(^image.QOI_Info); ok {
|
||||
free(v)
|
||||
}
|
||||
free(img)
|
||||
}
|
||||
|
||||
QOI_Opcode_Tag :: enum u8 {
|
||||
// 2-bit tags
|
||||
INDEX = 0b0000_0000, // 6-bit index into color array follows
|
||||
DIFF = 0b0100_0000, // 3x (RGB) 2-bit difference follows (-2..1), bias of 2.
|
||||
LUMA = 0b1000_0000, // Luma difference
|
||||
RUN = 0b1100_0000, // Run length encoding, bias -1
|
||||
|
||||
// 8-bit tags
|
||||
RGB = 0b1111_1110, // Raw RGB pixel follows
|
||||
RGBA = 0b1111_1111, // Raw RGBA pixel follows
|
||||
}
|
||||
|
||||
QOI_Opcode_Mask :: 0b1100_0000
|
||||
QOI_Data_Mask :: 0b0011_1111
|
||||
|
||||
qoi_hash :: #force_inline proc(pixel: RGBA_Pixel) -> (index: u8) {
|
||||
i1 := u16(pixel.r) * 3
|
||||
i2 := u16(pixel.g) * 5
|
||||
i3 := u16(pixel.b) * 7
|
||||
i4 := u16(pixel.a) * 11
|
||||
|
||||
return u8((i1 + i2 + i3 + i4) & 63)
|
||||
}
|
||||
|
||||
@(init, private)
|
||||
_register :: proc() {
|
||||
image.register(.QOI, load_from_bytes, destroy)
|
||||
/*
|
||||
Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
|
||||
Made available under Odin's BSD-3 license.
|
||||
|
||||
List of contributors:
|
||||
Jeroen van Rijn: Initial implementation.
|
||||
*/
|
||||
|
||||
|
||||
// package qoi implements a QOI image reader
|
||||
//
|
||||
// The QOI specification is at https://qoiformat.org.
|
||||
package qoi
|
||||
|
||||
import "core:image"
|
||||
import "core:compress"
|
||||
import "core:bytes"
|
||||
|
||||
Error :: image.Error
|
||||
Image :: image.Image
|
||||
Options :: image.Options
|
||||
|
||||
RGB_Pixel :: image.RGB_Pixel
|
||||
RGBA_Pixel :: image.RGBA_Pixel
|
||||
|
||||
save_to_buffer :: proc(output: ^bytes.Buffer, img: ^Image, options := Options{}, allocator := context.allocator) -> (err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
if img == nil {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
if output == nil {
|
||||
return .Invalid_Output
|
||||
}
|
||||
|
||||
pixels := img.width * img.height
|
||||
if pixels == 0 || pixels > image.MAX_DIMENSIONS {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
// QOI supports only 8-bit images with 3 or 4 channels.
|
||||
if img.depth != 8 || img.channels < 3 || img.channels > 4 {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
if img.channels * pixels != len(img.pixels.buf) {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
written := 0
|
||||
|
||||
// Calculate and allocate maximum size. We'll reclaim space to actually written output at the end.
|
||||
max_size := pixels * (img.channels + 1) + size_of(image.QOI_Header) + size_of(u64be)
|
||||
|
||||
if resize(&output.buf, max_size) != nil {
|
||||
return .Unable_To_Allocate_Or_Resize
|
||||
}
|
||||
|
||||
header := image.QOI_Header{
|
||||
magic = image.QOI_Magic,
|
||||
width = u32be(img.width),
|
||||
height = u32be(img.height),
|
||||
channels = u8(img.channels),
|
||||
color_space = .Linear if .qoi_all_channels_linear in options else .sRGB,
|
||||
}
|
||||
header_bytes := transmute([size_of(image.QOI_Header)]u8)header
|
||||
|
||||
copy(output.buf[written:], header_bytes[:])
|
||||
written += size_of(image.QOI_Header)
|
||||
|
||||
/*
|
||||
Encode loop starts here.
|
||||
*/
|
||||
seen: [64]RGBA_Pixel
|
||||
pix := RGBA_Pixel{0, 0, 0, 255}
|
||||
prev := pix
|
||||
|
||||
input := img.pixels.buf[:]
|
||||
run := u8(0)
|
||||
|
||||
for len(input) > 0 {
|
||||
if img.channels == 4 {
|
||||
pix = (^RGBA_Pixel)(raw_data(input))^
|
||||
} else {
|
||||
pix.rgb = (^RGB_Pixel)(raw_data(input))^
|
||||
}
|
||||
input = input[img.channels:]
|
||||
|
||||
if pix == prev {
|
||||
run += 1
|
||||
// As long as the pixel matches the last one, accumulate the run total.
|
||||
// If we reach the max run length or the end of the image, write the run.
|
||||
if run == 62 || len(input) == 0 {
|
||||
// Encode and write run
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
|
||||
written += 1
|
||||
run = 0
|
||||
}
|
||||
} else {
|
||||
if run > 0 {
|
||||
// The pixel differs from the previous one, but we still need to write the pending run.
|
||||
// Encode and write run
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
|
||||
written += 1
|
||||
run = 0
|
||||
}
|
||||
|
||||
index := qoi_hash(pix)
|
||||
|
||||
if seen[index] == pix {
|
||||
// Write indexed pixel
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.INDEX) | index
|
||||
written += 1
|
||||
} else {
|
||||
// Add pixel to index
|
||||
seen[index] = pix
|
||||
|
||||
// If the alpha matches the previous pixel's alpha, we don't need to write a full RGBA literal.
|
||||
if pix.a == prev.a {
|
||||
// Delta
|
||||
d := pix.rgb - prev.rgb
|
||||
|
||||
// DIFF, biased and modulo 256
|
||||
_d := d + 2
|
||||
|
||||
// LUMA, biased and modulo 256
|
||||
_l := RGB_Pixel{ d.r - d.g + 8, d.g + 32, d.b - d.g + 8 }
|
||||
|
||||
if _d.r < 4 && _d.g < 4 && _d.b < 4 {
|
||||
// Delta is between -2 and 1 inclusive
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.DIFF) | _d.r << 4 | _d.g << 2 | _d.b
|
||||
written += 1
|
||||
} else if _l.r < 16 && _l.g < 64 && _l.b < 16 {
|
||||
// Biased luma is between {-8..7, -32..31, -8..7}
|
||||
output.buf[written ] = u8(QOI_Opcode_Tag.LUMA) | _l.g
|
||||
output.buf[written + 1] = _l.r << 4 | _l.b
|
||||
written += 2
|
||||
} else {
|
||||
// Write RGB literal
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RGB)
|
||||
pix_bytes := transmute([4]u8)pix
|
||||
copy(output.buf[written + 1:], pix_bytes[:3])
|
||||
written += 4
|
||||
}
|
||||
} else {
|
||||
// Write RGBA literal
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RGBA)
|
||||
pix_bytes := transmute([4]u8)pix
|
||||
copy(output.buf[written + 1:], pix_bytes[:])
|
||||
written += 5
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = pix
|
||||
}
|
||||
|
||||
trailer := []u8{0, 0, 0, 0, 0, 0, 0, 1}
|
||||
copy(output.buf[written:], trailer[:])
|
||||
written += len(trailer)
|
||||
|
||||
resize(&output.buf, written)
|
||||
return nil
|
||||
}
|
||||
|
||||
load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
|
||||
ctx := &compress.Context_Memory_Input{
|
||||
input_data = data,
|
||||
}
|
||||
|
||||
img, err = load_from_context(ctx, options, allocator)
|
||||
return img, err
|
||||
}
|
||||
|
||||
@(optimization_mode="speed")
|
||||
load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
|
||||
context.allocator = allocator
|
||||
options := options
|
||||
|
||||
if .info in options {
|
||||
options |= {.return_metadata, .do_not_decompress_image}
|
||||
options -= {.info}
|
||||
}
|
||||
|
||||
if .return_header in options && .return_metadata in options {
|
||||
options -= {.return_header}
|
||||
}
|
||||
|
||||
header := image.read_data(ctx, image.QOI_Header) or_return
|
||||
if header.magic != image.QOI_Magic {
|
||||
return img, .Invalid_Signature
|
||||
}
|
||||
|
||||
if img == nil {
|
||||
img = new(Image)
|
||||
}
|
||||
img.which = .QOI
|
||||
|
||||
if .return_metadata in options {
|
||||
info := new(image.QOI_Info)
|
||||
info.header = header
|
||||
img.metadata = info
|
||||
}
|
||||
|
||||
if header.channels != 3 && header.channels != 4 {
|
||||
return img, .Invalid_Number_Of_Channels
|
||||
}
|
||||
|
||||
if header.color_space != .sRGB && header.color_space != .Linear {
|
||||
return img, .Invalid_Color_Space
|
||||
}
|
||||
|
||||
if header.width == 0 || header.height == 0 {
|
||||
return img, .Invalid_Image_Dimensions
|
||||
}
|
||||
|
||||
total_pixels := header.width * header.height
|
||||
if total_pixels > image.MAX_DIMENSIONS {
|
||||
return img, .Image_Dimensions_Too_Large
|
||||
}
|
||||
|
||||
img.width = int(header.width)
|
||||
img.height = int(header.height)
|
||||
img.channels = 4 if .alpha_add_if_missing in options else int(header.channels)
|
||||
img.depth = 8
|
||||
|
||||
if .do_not_decompress_image in options {
|
||||
img.channels = int(header.channels)
|
||||
return
|
||||
}
|
||||
|
||||
bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), img.channels, 8)
|
||||
|
||||
if resize(&img.pixels.buf, bytes_needed) != nil {
|
||||
return img, .Unable_To_Allocate_Or_Resize
|
||||
}
|
||||
|
||||
/*
|
||||
Decode loop starts here.
|
||||
*/
|
||||
seen: [64]RGBA_Pixel
|
||||
pix := RGBA_Pixel{0, 0, 0, 255}
|
||||
pixels := img.pixels.buf[:]
|
||||
|
||||
decode: for len(pixels) > 0 {
|
||||
data := image.read_u8(ctx) or_return
|
||||
|
||||
tag := QOI_Opcode_Tag(data)
|
||||
#partial switch tag {
|
||||
case .RGB:
|
||||
pix.rgb = image.read_data(ctx, RGB_Pixel) or_return
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .RGBA:
|
||||
pix = image.read_data(ctx, RGBA_Pixel) or_return
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case:
|
||||
// 2-bit tag
|
||||
tag = QOI_Opcode_Tag(data & QOI_Opcode_Mask)
|
||||
#partial switch tag {
|
||||
case .INDEX:
|
||||
pix = seen[data & 63]
|
||||
|
||||
case .DIFF:
|
||||
diff_r := ((data >> 4) & 3) - 2
|
||||
diff_g := ((data >> 2) & 3) - 2
|
||||
diff_b := ((data >> 0) & 3) - 2
|
||||
|
||||
pix += {diff_r, diff_g, diff_b, 0}
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .LUMA:
|
||||
data2 := image.read_u8(ctx) or_return
|
||||
|
||||
diff_g := (data & 63) - 32
|
||||
diff_r := diff_g - 8 + ((data2 >> 4) & 15)
|
||||
diff_b := diff_g - 8 + (data2 & 15)
|
||||
|
||||
pix += {diff_r, diff_g, diff_b, 0}
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .RUN:
|
||||
if length := int(data & 63) + 1; (length * img.channels) > len(pixels) {
|
||||
return img, .Corrupt
|
||||
} else {
|
||||
#no_bounds_check for _ in 0..<length {
|
||||
copy(pixels, pix[:img.channels])
|
||||
pixels = pixels[img.channels:]
|
||||
}
|
||||
}
|
||||
|
||||
continue decode
|
||||
|
||||
case:
|
||||
unreachable()
|
||||
}
|
||||
}
|
||||
|
||||
#no_bounds_check {
|
||||
copy(pixels, pix[:img.channels])
|
||||
pixels = pixels[img.channels:]
|
||||
}
|
||||
}
|
||||
|
||||
// The byte stream's end is marked with 7 0x00 bytes followed by a single 0x01 byte.
|
||||
trailer, trailer_err := compress.read_data(ctx, u64be)
|
||||
if trailer_err != nil || trailer != 0x1 {
|
||||
return img, .Missing_Or_Corrupt_Trailer
|
||||
}
|
||||
|
||||
if .alpha_premultiply in options && !image.alpha_drop_if_present(img, options) {
|
||||
return img, .Post_Processing_Error
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Cleanup of image-specific data.
|
||||
*/
|
||||
destroy :: proc(img: ^Image) {
|
||||
if img == nil {
|
||||
/*
|
||||
Nothing to do.
|
||||
Load must've returned with an error.
|
||||
*/
|
||||
return
|
||||
}
|
||||
|
||||
bytes.buffer_destroy(&img.pixels)
|
||||
|
||||
if v, ok := img.metadata.(^image.QOI_Info); ok {
|
||||
free(v)
|
||||
}
|
||||
free(img)
|
||||
}
|
||||
|
||||
QOI_Opcode_Tag :: enum u8 {
|
||||
// 2-bit tags
|
||||
INDEX = 0b0000_0000, // 6-bit index into color array follows
|
||||
DIFF = 0b0100_0000, // 3x (RGB) 2-bit difference follows (-2..1), bias of 2.
|
||||
LUMA = 0b1000_0000, // Luma difference
|
||||
RUN = 0b1100_0000, // Run length encoding, bias -1
|
||||
|
||||
// 8-bit tags
|
||||
RGB = 0b1111_1110, // Raw RGB pixel follows
|
||||
RGBA = 0b1111_1111, // Raw RGBA pixel follows
|
||||
}
|
||||
|
||||
QOI_Opcode_Mask :: 0b1100_0000
|
||||
QOI_Data_Mask :: 0b0011_1111
|
||||
|
||||
qoi_hash :: #force_inline proc(pixel: RGBA_Pixel) -> (index: u8) {
|
||||
i1 := u16(pixel.r) * 3
|
||||
i2 := u16(pixel.g) * 5
|
||||
i3 := u16(pixel.b) * 7
|
||||
i4 := u16(pixel.a) * 11
|
||||
|
||||
return u8((i1 + i2 + i3 + i4) & 63)
|
||||
}
|
||||
|
||||
@(init, private)
|
||||
_register :: proc() {
|
||||
image.register(.QOI, load_from_bytes, destroy)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package odin_format
|
||||
|
||||
#panic("The format package has been deprecated. Please look at https://github.com/DanielGavin/ols")
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package odin_printer
|
||||
|
||||
#panic("The printer package has been deprecated. Please look at https://github.com/DanielGavin/ols")
|
||||
@@ -1,922 +0,0 @@
|
||||
package odin_printer
|
||||
|
||||
import "core:odin/ast"
|
||||
import "core:odin/tokenizer"
|
||||
import "core:strings"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
|
||||
Type_Enum :: enum {Line_Comment, Value_Decl, Switch_Stmt, Struct, Assign, Call, Enum, If, For, Proc_Lit}
|
||||
|
||||
Line_Type :: bit_set[Type_Enum]
|
||||
|
||||
/*
|
||||
Represents an unwrapped line
|
||||
*/
|
||||
Line :: struct {
|
||||
format_tokens: [dynamic]Format_Token,
|
||||
finalized: bool,
|
||||
used: bool,
|
||||
depth: int,
|
||||
types: Line_Type, //for performance, so you don't have to verify what types are in it by going through the tokens - might give problems when adding linebreaking
|
||||
}
|
||||
|
||||
/*
|
||||
Represents a singular token in a unwrapped line
|
||||
*/
|
||||
Format_Token :: struct {
|
||||
kind: tokenizer.Token_Kind,
|
||||
text: string,
|
||||
type: Type_Enum,
|
||||
spaces_before: int,
|
||||
parameter_count: int,
|
||||
}
|
||||
|
||||
Printer :: struct {
|
||||
string_builder: strings.Builder,
|
||||
config: Config,
|
||||
depth: int, //the identation depth
|
||||
comments: [dynamic]^ast.Comment_Group,
|
||||
latest_comment_index: int,
|
||||
allocator: mem.Allocator,
|
||||
file: ^ast.File,
|
||||
source_position: tokenizer.Pos,
|
||||
last_source_position: tokenizer.Pos,
|
||||
lines: [dynamic]Line, //need to look into a better data structure, one that can handle inserting lines rather than appending
|
||||
skip_semicolon: bool,
|
||||
current_line: ^Line,
|
||||
current_line_index: int,
|
||||
last_line_index: int,
|
||||
last_token: ^Format_Token,
|
||||
merge_next_token: bool,
|
||||
space_next_token: bool,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
Config :: struct {
|
||||
spaces: int, //Spaces per indentation
|
||||
newline_limit: int, //The limit of newlines between statements and declarations.
|
||||
tabs: bool, //Enable or disable tabs
|
||||
convert_do: bool, //Convert all do statements to brace blocks
|
||||
semicolons: bool, //Enable semicolons
|
||||
split_multiple_stmts: bool,
|
||||
align_switch: bool,
|
||||
brace_style: Brace_Style,
|
||||
align_assignments: bool,
|
||||
align_structs: bool,
|
||||
align_style: Alignment_Style,
|
||||
align_enums: bool,
|
||||
align_length_break: int,
|
||||
indent_cases: bool,
|
||||
newline_style: Newline_Style,
|
||||
}
|
||||
|
||||
Brace_Style :: enum {
|
||||
_1TBS,
|
||||
Allman,
|
||||
Stroustrup,
|
||||
K_And_R,
|
||||
}
|
||||
|
||||
Block_Type :: enum {
|
||||
None,
|
||||
If_Stmt,
|
||||
Proc,
|
||||
Generic,
|
||||
Comp_Lit,
|
||||
Switch_Stmt,
|
||||
}
|
||||
|
||||
Alignment_Style :: enum {
|
||||
Align_On_Type_And_Equals,
|
||||
Align_On_Colon_And_Equals,
|
||||
}
|
||||
|
||||
Newline_Style :: enum {
|
||||
CRLF,
|
||||
LF,
|
||||
}
|
||||
|
||||
default_style := Config {
|
||||
spaces = 4,
|
||||
newline_limit = 2,
|
||||
convert_do = false,
|
||||
semicolons = false,
|
||||
tabs = true,
|
||||
brace_style = ._1TBS,
|
||||
split_multiple_stmts = true,
|
||||
align_assignments = true,
|
||||
align_style = .Align_On_Type_And_Equals,
|
||||
indent_cases = false,
|
||||
align_switch = true,
|
||||
align_structs = true,
|
||||
align_enums = true,
|
||||
newline_style = .CRLF,
|
||||
align_length_break = 9,
|
||||
}
|
||||
|
||||
make_printer :: proc(config: Config, allocator := context.allocator) -> Printer {
|
||||
return {
|
||||
config = config,
|
||||
allocator = allocator,
|
||||
debug = false,
|
||||
}
|
||||
}
|
||||
|
||||
print :: proc(p: ^Printer, file: ^ast.File) -> string {
|
||||
p.comments = file.comments
|
||||
|
||||
if len(file.decls) > 0 {
|
||||
p.lines = make([dynamic]Line, 0, (file.decls[len(file.decls) - 1].end.line - file.decls[0].pos.line) * 2, context.temp_allocator)
|
||||
}
|
||||
|
||||
set_source_position(p, file.pkg_token.pos)
|
||||
|
||||
p.last_source_position.line = 1
|
||||
|
||||
set_line(p, 0)
|
||||
|
||||
push_generic_token(p, .Package, 0)
|
||||
push_ident_token(p, file.pkg_name, 1)
|
||||
|
||||
for decl in file.decls {
|
||||
visit_decl(p, cast(^ast.Decl)decl)
|
||||
}
|
||||
|
||||
if len(p.comments) > 0 {
|
||||
infinite := p.comments[len(p.comments) - 1].end
|
||||
infinite.offset = 9999999
|
||||
push_comments(p, infinite)
|
||||
}
|
||||
|
||||
fix_lines(p)
|
||||
|
||||
builder := strings.builder_make(0, 5 * mem.Megabyte, p.allocator)
|
||||
|
||||
last_line := 0
|
||||
|
||||
newline: string
|
||||
|
||||
if p.config.newline_style == .LF {
|
||||
newline = "\n"
|
||||
} else {
|
||||
newline = "\r\n"
|
||||
}
|
||||
|
||||
for line, line_index in p.lines {
|
||||
diff_line := line_index - last_line
|
||||
|
||||
for i := 0; i < diff_line; i += 1 {
|
||||
strings.write_string(&builder, newline)
|
||||
}
|
||||
|
||||
if p.config.tabs {
|
||||
for i := 0; i < line.depth; i += 1 {
|
||||
strings.write_byte(&builder, '\t')
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < line.depth * p.config.spaces; i += 1 {
|
||||
strings.write_byte(&builder, ' ')
|
||||
}
|
||||
}
|
||||
|
||||
if p.debug {
|
||||
strings.write_string(&builder, fmt.tprintf("line %v: ", line_index))
|
||||
}
|
||||
|
||||
for format_token in line.format_tokens {
|
||||
|
||||
for i := 0; i < format_token.spaces_before; i += 1 {
|
||||
strings.write_byte(&builder, ' ')
|
||||
}
|
||||
|
||||
strings.write_string(&builder, format_token.text)
|
||||
}
|
||||
|
||||
last_line = line_index
|
||||
}
|
||||
|
||||
strings.write_string(&builder, newline)
|
||||
|
||||
return strings.to_string(builder)
|
||||
}
|
||||
|
||||
fix_lines :: proc(p: ^Printer) {
|
||||
align_var_decls(p)
|
||||
format_generic(p)
|
||||
align_comments(p) //align them last since they rely on the other alignments
|
||||
}
|
||||
|
||||
format_value_decl :: proc(p: ^Printer, index: int) {
|
||||
|
||||
eq_found := false
|
||||
eq_token: Format_Token
|
||||
eq_line: int
|
||||
largest := 0
|
||||
|
||||
found_eq: for line, line_index in p.lines[index:] {
|
||||
for format_token in line.format_tokens {
|
||||
|
||||
largest += len(format_token.text) + format_token.spaces_before
|
||||
|
||||
if format_token.kind == .Eq {
|
||||
eq_token = format_token
|
||||
eq_line = line_index + index
|
||||
eq_found = true
|
||||
break found_eq
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !eq_found {
|
||||
return
|
||||
}
|
||||
|
||||
align_next := false
|
||||
|
||||
//check to see if there is a binary operator in the last token(this is guaranteed by the ast visit), otherwise it's not multilined
|
||||
for line in p.lines[eq_line:] {
|
||||
|
||||
if len(line.format_tokens) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if align_next {
|
||||
line.format_tokens[0].spaces_before = largest + 1
|
||||
align_next = false
|
||||
}
|
||||
|
||||
kind := find_last_token(line.format_tokens).kind
|
||||
|
||||
if tokenizer.Token_Kind.B_Operator_Begin < kind && kind <= tokenizer.Token_Kind.Cmp_Or {
|
||||
align_next = true
|
||||
}
|
||||
|
||||
if !align_next {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
find_last_token :: proc(format_tokens: [dynamic]Format_Token) -> Format_Token {
|
||||
|
||||
for i := len(format_tokens) - 1; i >= 0; i -= 1 {
|
||||
|
||||
if format_tokens[i].kind != .Comment {
|
||||
return format_tokens[i]
|
||||
}
|
||||
}
|
||||
|
||||
panic("not possible")
|
||||
}
|
||||
|
||||
format_assignment :: proc(p: ^Printer, index: int) {
|
||||
}
|
||||
|
||||
format_call :: proc(p: ^Printer, line_index: int, format_index: int) {
|
||||
|
||||
paren_found := false
|
||||
paren_token: Format_Token
|
||||
paren_line: int
|
||||
paren_token_index: int
|
||||
largest := 0
|
||||
|
||||
found_paren: for line, i in p.lines[line_index:] {
|
||||
for format_token, j in line.format_tokens {
|
||||
|
||||
largest += len(format_token.text) + format_token.spaces_before
|
||||
|
||||
if i == 0 && j < format_index {
|
||||
continue
|
||||
}
|
||||
|
||||
if format_token.kind == .Open_Paren && format_token.type == .Call {
|
||||
paren_token = format_token
|
||||
paren_line = line_index + i
|
||||
paren_found = true
|
||||
paren_token_index = j
|
||||
break found_paren
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !paren_found {
|
||||
panic("Should not be possible")
|
||||
}
|
||||
|
||||
paren_count := 1
|
||||
done := false
|
||||
|
||||
for line in p.lines[paren_line:] {
|
||||
|
||||
if len(line.format_tokens) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for format_token, i in line.format_tokens {
|
||||
|
||||
if format_token.kind == .Comment {
|
||||
continue
|
||||
}
|
||||
|
||||
if line_index == 0 && i <= paren_token_index {
|
||||
continue
|
||||
}
|
||||
|
||||
if format_token.kind == .Open_Paren {
|
||||
paren_count += 1
|
||||
} else if format_token.kind == .Close_Paren {
|
||||
paren_count -= 1
|
||||
}
|
||||
|
||||
if paren_count == 0 {
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
if line_index != 0 {
|
||||
line.format_tokens[0].spaces_before = largest
|
||||
}
|
||||
|
||||
if done {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
format_keyword_to_brace :: proc(p: ^Printer, line_index: int, format_index: int, keyword: tokenizer.Token_Kind) {
|
||||
|
||||
keyword_found := false
|
||||
keyword_token: Format_Token
|
||||
keyword_line: int
|
||||
|
||||
largest := 0
|
||||
brace_count := 0
|
||||
done := false
|
||||
|
||||
found_keyword: for line, i in p.lines[line_index:] {
|
||||
for format_token in line.format_tokens {
|
||||
|
||||
largest += len(format_token.text) + format_token.spaces_before
|
||||
|
||||
if format_token.kind == keyword {
|
||||
keyword_token = format_token
|
||||
keyword_line = line_index + i
|
||||
keyword_found = true
|
||||
break found_keyword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !keyword_found {
|
||||
panic("Should not be possible")
|
||||
}
|
||||
|
||||
for line, line_idx in p.lines[keyword_line:] {
|
||||
|
||||
if len(line.format_tokens) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for format_token, i in line.format_tokens {
|
||||
|
||||
if format_token.kind == .Comment {
|
||||
break
|
||||
} else if format_token.kind == .Undef {
|
||||
return
|
||||
}
|
||||
|
||||
if line_idx == 0 && i <= format_index {
|
||||
continue
|
||||
}
|
||||
|
||||
if format_token.kind == .Open_Brace {
|
||||
brace_count += 1
|
||||
} else if format_token.kind == .Close_Brace {
|
||||
brace_count -= 1
|
||||
}
|
||||
|
||||
if brace_count == 1 {
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
if line_idx != 0 {
|
||||
line.format_tokens[0].spaces_before = largest + 1
|
||||
}
|
||||
|
||||
if done {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
format_generic :: proc(p: ^Printer) {
|
||||
next_struct_line := 0
|
||||
|
||||
for line, line_index in p.lines {
|
||||
|
||||
if len(line.format_tokens) <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for format_token, token_index in line.format_tokens {
|
||||
#partial switch format_token.kind {
|
||||
case .For, .If, .When, .Switch:
|
||||
format_keyword_to_brace(p, line_index, token_index, format_token.kind)
|
||||
case .Proc:
|
||||
if format_token.type == .Proc_Lit {
|
||||
format_keyword_to_brace(p, line_index, token_index, format_token.kind)
|
||||
}
|
||||
case:
|
||||
if format_token.type == .Call {
|
||||
format_call(p, line_index, token_index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if .Switch_Stmt in line.types && p.config.align_switch {
|
||||
align_switch_stmt(p, line_index)
|
||||
}
|
||||
|
||||
if .Enum in line.types && p.config.align_enums {
|
||||
align_enum(p, line_index)
|
||||
}
|
||||
|
||||
if .Struct in line.types && p.config.align_structs && next_struct_line <= 0 {
|
||||
next_struct_line = align_struct(p, line_index)
|
||||
}
|
||||
|
||||
if .Value_Decl in line.types {
|
||||
format_value_decl(p, line_index)
|
||||
}
|
||||
|
||||
if .Assign in line.types {
|
||||
format_assignment(p, line_index)
|
||||
}
|
||||
|
||||
next_struct_line -= 1
|
||||
}
|
||||
}
|
||||
|
||||
align_var_decls :: proc(p: ^Printer) {
|
||||
|
||||
current_line: int
|
||||
current_typed: bool
|
||||
current_not_mutable: bool
|
||||
|
||||
largest_lhs := 0
|
||||
largest_rhs := 0
|
||||
|
||||
TokenAndLength :: struct {
|
||||
format_token: ^Format_Token,
|
||||
length: int,
|
||||
}
|
||||
|
||||
colon_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
|
||||
type_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
|
||||
equal_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
|
||||
|
||||
for line, line_index in p.lines {
|
||||
|
||||
//It is only possible to align value decls that are one one line, otherwise just ignore them
|
||||
if .Value_Decl not_in line.types {
|
||||
continue
|
||||
}
|
||||
|
||||
typed := true
|
||||
not_mutable := false
|
||||
continue_flag := false
|
||||
|
||||
for i := 0; i < len(line.format_tokens); i += 1 {
|
||||
if line.format_tokens[i].kind == .Colon && line.format_tokens[min(i + 1, len(line.format_tokens) - 1)].kind == .Eq {
|
||||
typed = false
|
||||
}
|
||||
|
||||
if line.format_tokens[i].kind == .Colon && line.format_tokens[min(i + 1, len(line.format_tokens) - 1)].kind == .Colon {
|
||||
not_mutable = true
|
||||
}
|
||||
|
||||
if line.format_tokens[i].kind == .Union ||
|
||||
line.format_tokens[i].kind == .Enum ||
|
||||
line.format_tokens[i].kind == .Struct ||
|
||||
line.format_tokens[i].kind == .For ||
|
||||
line.format_tokens[i].kind == .If ||
|
||||
line.format_tokens[i].kind == .Comment {
|
||||
continue_flag = true
|
||||
}
|
||||
|
||||
//enforced undef is always on the last line, if it exists
|
||||
if line.format_tokens[i].kind == .Proc && line.format_tokens[len(line.format_tokens)-1].kind != .Undef {
|
||||
continue_flag = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if continue_flag {
|
||||
continue
|
||||
}
|
||||
|
||||
if line_index != current_line + 1 || typed != current_typed || not_mutable != current_not_mutable {
|
||||
|
||||
if p.config.align_style == .Align_On_Colon_And_Equals || !current_typed || current_not_mutable {
|
||||
for colon_token in colon_tokens {
|
||||
colon_token.format_token.spaces_before = largest_lhs - colon_token.length + 1
|
||||
}
|
||||
} else if p.config.align_style == .Align_On_Type_And_Equals {
|
||||
for type_token in type_tokens {
|
||||
type_token.format_token.spaces_before = largest_lhs - type_token.length + 1
|
||||
}
|
||||
}
|
||||
|
||||
if current_typed {
|
||||
for equal_token in equal_tokens {
|
||||
equal_token.format_token.spaces_before = largest_rhs - equal_token.length + 1
|
||||
}
|
||||
} else {
|
||||
for equal_token in equal_tokens {
|
||||
equal_token.format_token.spaces_before = 0
|
||||
}
|
||||
}
|
||||
|
||||
clear(&colon_tokens)
|
||||
clear(&type_tokens)
|
||||
clear(&equal_tokens)
|
||||
|
||||
largest_rhs = 0
|
||||
largest_lhs = 0
|
||||
current_typed = typed
|
||||
current_not_mutable = not_mutable
|
||||
}
|
||||
|
||||
current_line = line_index
|
||||
|
||||
current_token_index := 0
|
||||
lhs_length := 0
|
||||
rhs_length := 0
|
||||
|
||||
//calcuate the length of lhs of a value decl i.e. `a, b:`
|
||||
for; current_token_index < len(line.format_tokens); current_token_index += 1 {
|
||||
|
||||
lhs_length += len(line.format_tokens[current_token_index].text) + line.format_tokens[current_token_index].spaces_before
|
||||
|
||||
if line.format_tokens[current_token_index].kind == .Colon {
|
||||
append(&colon_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index], length = lhs_length})
|
||||
|
||||
if len(line.format_tokens) > current_token_index && line.format_tokens[current_token_index + 1].kind != .Eq {
|
||||
append(&type_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index + 1], length = lhs_length})
|
||||
}
|
||||
|
||||
current_token_index += 1
|
||||
largest_lhs = max(largest_lhs, lhs_length)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//calcuate the length of the rhs i.e. `[dynamic]int = 123123`
|
||||
for; current_token_index < len(line.format_tokens); current_token_index += 1 {
|
||||
|
||||
rhs_length += len(line.format_tokens[current_token_index].text) + line.format_tokens[current_token_index].spaces_before
|
||||
|
||||
if line.format_tokens[current_token_index].kind == .Eq {
|
||||
append(&equal_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index], length = rhs_length})
|
||||
largest_rhs = max(largest_rhs, rhs_length)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//repeating myself, move to sub procedure
|
||||
if p.config.align_style == .Align_On_Colon_And_Equals || !current_typed || current_not_mutable {
|
||||
for colon_token in colon_tokens {
|
||||
colon_token.format_token.spaces_before = largest_lhs - colon_token.length + 1
|
||||
}
|
||||
} else if p.config.align_style == .Align_On_Type_And_Equals {
|
||||
for type_token in type_tokens {
|
||||
type_token.format_token.spaces_before = largest_lhs - type_token.length + 1
|
||||
}
|
||||
}
|
||||
|
||||
if current_typed {
|
||||
for equal_token in equal_tokens {
|
||||
equal_token.format_token.spaces_before = largest_rhs - equal_token.length + 1
|
||||
}
|
||||
} else {
|
||||
for equal_token in equal_tokens {
|
||||
equal_token.format_token.spaces_before = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
align_switch_stmt :: proc(p: ^Printer, index: int) {
|
||||
switch_found := false
|
||||
brace_token: Format_Token
|
||||
brace_line: int
|
||||
|
||||
found_switch_brace: for line, line_index in p.lines[index:] {
|
||||
for format_token in line.format_tokens {
|
||||
if format_token.kind == .Open_Brace && switch_found {
|
||||
brace_token = format_token
|
||||
brace_line = line_index + index
|
||||
break found_switch_brace
|
||||
} else if format_token.kind == .Open_Brace {
|
||||
break
|
||||
} else if format_token.kind == .Switch {
|
||||
switch_found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !switch_found {
|
||||
return
|
||||
}
|
||||
|
||||
largest := 0
|
||||
case_count := 0
|
||||
|
||||
TokenAndLength :: struct {
|
||||
format_token: ^Format_Token,
|
||||
length: int,
|
||||
}
|
||||
|
||||
format_tokens := make([dynamic]TokenAndLength, 0, brace_token.parameter_count, context.temp_allocator)
|
||||
|
||||
//find all the switch cases that are one lined
|
||||
for line in p.lines[brace_line + 1:] {
|
||||
|
||||
case_found := false
|
||||
colon_found := false
|
||||
length := 0
|
||||
|
||||
for format_token, i in line.format_tokens {
|
||||
|
||||
if format_token.kind == .Comment {
|
||||
break
|
||||
}
|
||||
|
||||
//this will only happen if the case is one lined
|
||||
if case_found && colon_found {
|
||||
append(&format_tokens, TokenAndLength {format_token = &line.format_tokens[i], length = length})
|
||||
largest = max(length, largest)
|
||||
break
|
||||
}
|
||||
|
||||
if format_token.kind == .Case {
|
||||
case_found = true
|
||||
case_count += 1
|
||||
} else if format_token.kind == .Colon {
|
||||
colon_found = true
|
||||
}
|
||||
|
||||
length += len(format_token.text) + format_token.spaces_before
|
||||
}
|
||||
|
||||
if case_count >= brace_token.parameter_count {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for token in format_tokens {
|
||||
token.format_token.spaces_before = largest - token.length + 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
align_enum :: proc(p: ^Printer, index: int) {
|
||||
enum_found := false
|
||||
brace_token: Format_Token
|
||||
brace_line: int
|
||||
|
||||
found_enum_brace: for line, line_index in p.lines[index:] {
|
||||
for format_token in line.format_tokens {
|
||||
if format_token.kind == .Open_Brace && enum_found {
|
||||
brace_token = format_token
|
||||
brace_line = line_index + index
|
||||
break found_enum_brace
|
||||
} else if format_token.kind == .Open_Brace {
|
||||
break
|
||||
} else if format_token.kind == .Enum {
|
||||
enum_found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !enum_found {
|
||||
return
|
||||
}
|
||||
|
||||
largest := 0
|
||||
comma_count := 0
|
||||
|
||||
TokenAndLength :: struct {
|
||||
format_token: ^Format_Token,
|
||||
length: int,
|
||||
}
|
||||
|
||||
format_tokens := make([dynamic]TokenAndLength, 0, brace_token.parameter_count, context.temp_allocator)
|
||||
|
||||
for line in p.lines[brace_line + 1:] {
|
||||
length := 0
|
||||
|
||||
for format_token, i in line.format_tokens {
|
||||
if format_token.kind == .Comment {
|
||||
break
|
||||
}
|
||||
|
||||
if format_token.kind == .Eq {
|
||||
append(&format_tokens, TokenAndLength {format_token = &line.format_tokens[i], length = length})
|
||||
largest = max(length, largest)
|
||||
break
|
||||
} else if format_token.kind == .Comma {
|
||||
comma_count += 1
|
||||
}
|
||||
|
||||
length += len(format_token.text) + format_token.spaces_before
|
||||
}
|
||||
|
||||
if comma_count >= brace_token.parameter_count {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for token in format_tokens {
|
||||
token.format_token.spaces_before = largest - token.length + 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
align_struct :: proc(p: ^Printer, index: int) -> int {
|
||||
struct_found := false
|
||||
brace_token: Format_Token
|
||||
brace_line: int
|
||||
|
||||
found_struct_brace: for line, line_index in p.lines[index:] {
|
||||
for format_token in line.format_tokens {
|
||||
if format_token.kind == .Open_Brace && struct_found {
|
||||
brace_token = format_token
|
||||
brace_line = line_index + index
|
||||
break found_struct_brace
|
||||
} else if format_token.kind == .Open_Brace {
|
||||
break
|
||||
} else if format_token.kind == .Struct {
|
||||
struct_found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !struct_found {
|
||||
return 0
|
||||
}
|
||||
|
||||
largest := 0
|
||||
colon_count := 0
|
||||
nested := false
|
||||
seen_brace := false
|
||||
|
||||
TokenAndLength :: struct {
|
||||
format_token: ^Format_Token,
|
||||
length: int,
|
||||
}
|
||||
|
||||
format_tokens := make([]TokenAndLength, brace_token.parameter_count, context.temp_allocator)
|
||||
|
||||
if brace_token.parameter_count == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
end_line_index := 0
|
||||
|
||||
for line, line_index in p.lines[brace_line + 1:] {
|
||||
length := 0
|
||||
|
||||
for format_token, i in line.format_tokens {
|
||||
|
||||
//give up on nested structs
|
||||
if format_token.kind == .Comment {
|
||||
break
|
||||
} else if format_token.kind == .Open_Paren {
|
||||
break
|
||||
} else if format_token.kind == .Open_Brace {
|
||||
seen_brace = true
|
||||
} else if format_token.kind == .Close_Brace {
|
||||
seen_brace = false
|
||||
} else if seen_brace {
|
||||
continue
|
||||
}
|
||||
|
||||
if format_token.kind == .Colon {
|
||||
format_tokens[colon_count] = {format_token = &line.format_tokens[i + 1], length = length}
|
||||
|
||||
if format_tokens[colon_count].format_token.kind == .Struct {
|
||||
nested = true
|
||||
}
|
||||
|
||||
colon_count += 1
|
||||
largest = max(length, largest)
|
||||
}
|
||||
|
||||
length += len(format_token.text) + format_token.spaces_before
|
||||
}
|
||||
|
||||
if nested {
|
||||
end_line_index = line_index + brace_line + 1
|
||||
}
|
||||
|
||||
if colon_count >= brace_token.parameter_count {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//give up aligning nested, it never looks good
|
||||
if nested {
|
||||
for line, line_index in p.lines[end_line_index:] {
|
||||
for format_token in line.format_tokens {
|
||||
if format_token.kind == .Close_Brace {
|
||||
return end_line_index + line_index - index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for token in format_tokens {
|
||||
token.format_token.spaces_before = largest - token.length + 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
align_comments :: proc(p: ^Printer) {
|
||||
|
||||
Comment_Align_Info :: struct {
|
||||
length: int,
|
||||
begin: int,
|
||||
end: int,
|
||||
depth: int,
|
||||
}
|
||||
|
||||
comment_infos := make([dynamic]Comment_Align_Info, 0, context.temp_allocator)
|
||||
|
||||
current_info: Comment_Align_Info
|
||||
|
||||
for line, line_index in p.lines {
|
||||
if len(line.format_tokens) <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if .Line_Comment in line.types {
|
||||
if current_info.end + 1 != line_index || current_info.depth != line.depth ||
|
||||
(current_info.begin == current_info.end && current_info.length == 0) {
|
||||
|
||||
if (current_info.begin != 0 && current_info.end != 0) || current_info.length > 0 {
|
||||
append(&comment_infos, current_info)
|
||||
}
|
||||
|
||||
current_info.begin = line_index
|
||||
current_info.end = line_index
|
||||
current_info.depth = line.depth
|
||||
current_info.length = 0
|
||||
}
|
||||
|
||||
length := 0
|
||||
|
||||
for format_token in line.format_tokens {
|
||||
if format_token.kind == .Comment {
|
||||
current_info.length = max(current_info.length, length)
|
||||
current_info.end = line_index
|
||||
}
|
||||
|
||||
length += format_token.spaces_before + len(format_token.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current_info.begin != 0 && current_info.end != 0) || current_info.length > 0 {
|
||||
append(&comment_infos, current_info)
|
||||
}
|
||||
|
||||
for info in comment_infos {
|
||||
|
||||
if info.begin == info.end || info.length == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := info.begin; i <= info.end; i += 1 {
|
||||
l := p.lines[i]
|
||||
|
||||
length := 0
|
||||
|
||||
for format_token in l.format_tokens {
|
||||
if format_token.kind == .Comment {
|
||||
if len(l.format_tokens) == 1 {
|
||||
l.format_tokens[i].spaces_before = info.length + 1
|
||||
} else {
|
||||
l.format_tokens[i].spaces_before = info.length - length + 1
|
||||
}
|
||||
}
|
||||
|
||||
length += format_token.spaces_before + len(format_token.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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 ---
|
||||
}
|
||||
|
||||
@@ -183,16 +183,17 @@ undo_check :: proc(s: ^State) {
|
||||
}
|
||||
|
||||
// insert text into the edit state - deletes the current selection
|
||||
input_text :: proc(s: ^State, text: string) {
|
||||
input_text :: proc(s: ^State, text: string) -> int {
|
||||
if len(text) == 0 {
|
||||
return
|
||||
return 0
|
||||
}
|
||||
if has_selection(s) {
|
||||
selection_delete(s)
|
||||
}
|
||||
insert(s, s.selection[0], text)
|
||||
offset := s.selection[0] + len(text)
|
||||
n := insert(s, s.selection[0], text)
|
||||
offset := s.selection[0] + n
|
||||
s.selection = {offset, offset}
|
||||
return n
|
||||
}
|
||||
|
||||
// insert slice of runes into the edit state - deletes the current selection
|
||||
@@ -206,8 +207,11 @@ input_runes :: proc(s: ^State, text: []rune) {
|
||||
offset := s.selection[0]
|
||||
for r in text {
|
||||
b, w := utf8.encode_rune(r)
|
||||
insert(s, offset, string(b[:w]))
|
||||
offset += w
|
||||
n := insert(s, offset, string(b[:w]))
|
||||
offset += n
|
||||
if n != w {
|
||||
break
|
||||
}
|
||||
}
|
||||
s.selection = {offset, offset}
|
||||
}
|
||||
@@ -219,17 +223,29 @@ input_rune :: proc(s: ^State, r: rune) {
|
||||
}
|
||||
offset := s.selection[0]
|
||||
b, w := utf8.encode_rune(r)
|
||||
insert(s, offset, string(b[:w]))
|
||||
offset += w
|
||||
n := insert(s, offset, string(b[:w]))
|
||||
offset += n
|
||||
s.selection = {offset, offset}
|
||||
}
|
||||
|
||||
// insert a single rune into the edit state - deletes the current selection
|
||||
insert :: proc(s: ^State, at: int, text: string) {
|
||||
insert :: proc(s: ^State, at: int, text: string) -> int {
|
||||
undo_check(s)
|
||||
if s.builder != nil {
|
||||
inject_at(&s.builder.buf, at, text)
|
||||
if ok, _ := inject_at(&s.builder.buf, at, text); !ok {
|
||||
n := cap(s.builder.buf) - len(s.builder.buf)
|
||||
assert(n < len(text))
|
||||
for is_continuation_byte(text[n]) {
|
||||
n -= 1
|
||||
}
|
||||
if ok2, _ := inject_at(&s.builder.buf, at, text[:n]); !ok2 {
|
||||
n = 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
return len(text)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// remove the wanted range withing, usually the selection within byte indices
|
||||
@@ -263,11 +279,12 @@ selection_delete :: proc(s: ^State) {
|
||||
s.selection = {lo, lo}
|
||||
}
|
||||
|
||||
is_continuation_byte :: proc(b: byte) -> bool {
|
||||
return b >= 0x80 && b < 0xc0
|
||||
}
|
||||
|
||||
// translates the caret position
|
||||
translate_position :: proc(s: ^State, t: Translation) -> int {
|
||||
is_continuation_byte :: proc(b: byte) -> bool {
|
||||
return b >= 0x80 && b < 0xc0
|
||||
}
|
||||
is_space :: proc(b: byte) -> bool {
|
||||
return b == ' ' || b == '\t' || b == '\n'
|
||||
}
|
||||
|
||||
+36
-17
@@ -60,10 +60,6 @@ parse_mo_from_bytes :: proc(data: []byte, options := DEFAULT_PARSE_OPTIONS, plur
|
||||
translation.pluralize = pluralizer
|
||||
strings.intern_init(&translation.intern, allocator, allocator)
|
||||
|
||||
// Gettext MO files only have one section.
|
||||
translation.k_v[""] = {}
|
||||
section := &translation.k_v[""]
|
||||
|
||||
for n := u32(0); n < count; n += 1 {
|
||||
/*
|
||||
Grab string's original length and offset.
|
||||
@@ -83,37 +79,60 @@ parse_mo_from_bytes :: proc(data: []byte, options := DEFAULT_PARSE_OPTIONS, plur
|
||||
max_offset := int(max(o_offset + o_length + 1, t_offset + t_length + 1))
|
||||
if len(data) < max_offset { return translation, .Premature_EOF }
|
||||
|
||||
key := data[o_offset:][:o_length]
|
||||
val := data[t_offset:][:t_length]
|
||||
key_data := data[o_offset:][:o_length]
|
||||
val_data := data[t_offset:][:t_length]
|
||||
|
||||
/*
|
||||
Could be a pluralized string.
|
||||
*/
|
||||
zero := []byte{0}
|
||||
keys := bytes.split(key_data, zero); defer delete(keys)
|
||||
vals := bytes.split(val_data, zero); defer delete(vals)
|
||||
|
||||
keys := bytes.split(key, zero)
|
||||
vals := bytes.split(val, zero)
|
||||
|
||||
if len(keys) != len(vals) || max(len(keys), len(vals)) > MAX_PLURALS {
|
||||
if (len(keys) != 1 && len(keys) != 2) || len(vals) > MAX_PLURALS {
|
||||
return translation, .MO_File_Incorrect_Plural_Count
|
||||
}
|
||||
|
||||
for k in keys {
|
||||
interned_key, _ := strings.intern_get(&translation.intern, string(k))
|
||||
section_name := ""
|
||||
key := string(k)
|
||||
|
||||
interned_vals := make([]string, len(keys))
|
||||
// Scan for <context>EOT<key>
|
||||
for ch, i in k {
|
||||
if ch == 0x04 {
|
||||
section_name = string(k[:i])
|
||||
key = string(k[i+1:])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we merge sections, then all entries end in the "" context.
|
||||
if options.merge_sections {
|
||||
section_name = ""
|
||||
}
|
||||
|
||||
section_name, _ = strings.intern_get(&translation.intern, section_name)
|
||||
if section_name not_in translation.k_v {
|
||||
translation.k_v[section_name] = {}
|
||||
}
|
||||
|
||||
section := &translation.k_v[section_name]
|
||||
interned_key, _ := strings.intern_get(&translation.intern, string(key))
|
||||
|
||||
// Duplicate key should not be allowed.
|
||||
if interned_key in section {
|
||||
return translation, .Duplicate_Key
|
||||
}
|
||||
|
||||
interned_vals := make([]string, len(vals))
|
||||
last_val: string
|
||||
|
||||
i := 0
|
||||
for v in vals {
|
||||
for v, i in vals {
|
||||
interned_vals[i], _ = strings.intern_get(&translation.intern, string(v))
|
||||
last_val = interned_vals[i]
|
||||
i += 1
|
||||
}
|
||||
section[interned_key] = interned_vals
|
||||
}
|
||||
delete(vals)
|
||||
delete(keys)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -57,12 +57,12 @@ _rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_o
|
||||
(len(rfc_datetime) >= 20) or_return
|
||||
|
||||
// Scan and eat YYYY-MM-DD[Tt], then scan and eat HH:MM:SS, leave separator
|
||||
year := scan_digits(rfc_datetime[0:], "-", 4) or_return
|
||||
month := scan_digits(rfc_datetime[5:], "-", 2) or_return
|
||||
day := scan_digits(rfc_datetime[8:], "Tt", 2) or_return
|
||||
hour := scan_digits(rfc_datetime[11:], ":", 2) or_return
|
||||
minute := scan_digits(rfc_datetime[14:], ":", 2) or_return
|
||||
second := scan_digits(rfc_datetime[17:], "", 2) or_return
|
||||
year := scan_digits(rfc_datetime[0:], "-", 4) or_return
|
||||
month := scan_digits(rfc_datetime[5:], "-", 2) or_return
|
||||
day := scan_digits(rfc_datetime[8:], "Tt ", 2) or_return
|
||||
hour := scan_digits(rfc_datetime[11:], ":", 2) or_return
|
||||
minute := scan_digits(rfc_datetime[14:], ":", 2) or_return
|
||||
second := scan_digits(rfc_datetime[17:], "", 2) or_return
|
||||
nanos := 0
|
||||
count := 19
|
||||
|
||||
@@ -87,7 +87,7 @@ _rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_o
|
||||
|
||||
// Scan UTC offset
|
||||
switch rfc_datetime[count] {
|
||||
case 'Z':
|
||||
case 'Z', 'z':
|
||||
utc_offset = 0
|
||||
count += 1
|
||||
case '+', '-':
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//+build wasi
|
||||
package time
|
||||
|
||||
import wasi "core:sys/wasm/wasi"
|
||||
|
||||
_IS_SUPPORTED :: false
|
||||
|
||||
_now :: proc "contextless" () -> Time {
|
||||
|
||||
@@ -21,6 +21,7 @@ import queue "core:container/queue"
|
||||
import small_array "core:container/small_array"
|
||||
import lru "core:container/lru"
|
||||
import list "core:container/intrusive/list"
|
||||
import rbtree "core:container/rbtree"
|
||||
import topological_sort "core:container/topological_sort"
|
||||
|
||||
import crypto "core:crypto"
|
||||
@@ -91,9 +92,7 @@ import virtual "core:mem/virtual"
|
||||
|
||||
import ast "core:odin/ast"
|
||||
import doc_format "core:odin/doc-format"
|
||||
import odin_format "core:odin/format"
|
||||
import odin_parser "core:odin/parser"
|
||||
import odin_printer "core:odin/printer"
|
||||
import odin_tokenizer "core:odin/tokenizer"
|
||||
|
||||
import spall "core:prof/spall"
|
||||
@@ -147,6 +146,7 @@ _ :: queue
|
||||
_ :: small_array
|
||||
_ :: lru
|
||||
_ :: list
|
||||
_ :: rbtree
|
||||
_ :: topological_sort
|
||||
_ :: crypto
|
||||
_ :: crypto_hash
|
||||
@@ -207,9 +207,7 @@ _ :: mem
|
||||
_ :: virtual
|
||||
_ :: ast
|
||||
_ :: doc_format
|
||||
_ :: odin_format
|
||||
_ :: odin_parser
|
||||
_ :: odin_printer
|
||||
_ :: odin_tokenizer
|
||||
_ :: os
|
||||
_ :: spall
|
||||
|
||||
+1
-1
@@ -251,7 +251,7 @@ gb_internal void report_ram_info() {
|
||||
int result = sysinfo(&info);
|
||||
|
||||
if (result == 0x0) {
|
||||
gb_printf("%lu MiB\n", info.totalram * info.mem_unit / gb_megabytes(1));
|
||||
gb_printf("%lu MiB\n", (unsigned long)(info.totalram * info.mem_unit / gb_megabytes(1)));
|
||||
} else {
|
||||
gb_printf("Unknown.\n");
|
||||
}
|
||||
|
||||
+11
-9
@@ -978,7 +978,7 @@ gb_global TargetMetrics target_linux_arm32 = {
|
||||
TargetOs_linux,
|
||||
TargetArch_arm32,
|
||||
4, 4, 4, 8,
|
||||
str_lit("arm-linux-gnu"),
|
||||
str_lit("arm-unknown-linux-gnueabihf"),
|
||||
};
|
||||
|
||||
gb_global TargetMetrics target_darwin_amd64 = {
|
||||
@@ -1906,6 +1906,16 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta
|
||||
#else
|
||||
metrics = &target_linux_amd64;
|
||||
#endif
|
||||
#elif defined(GB_CPU_ARM)
|
||||
#if defined(GB_SYSTEM_WINDOWS)
|
||||
#error "Build Error: Unsupported architecture"
|
||||
#elif defined(GB_SYSTEM_OSX)
|
||||
#error "Build Error: Unsupported architecture"
|
||||
#elif defined(GB_SYSTEM_FREEBSD)
|
||||
#error "Build Error: Unsupported architecture"
|
||||
#else
|
||||
metrics = &target_linux_arm32;
|
||||
#endif
|
||||
#else
|
||||
#if defined(GB_SYSTEM_WINDOWS)
|
||||
metrics = &target_windows_i386;
|
||||
@@ -2052,14 +2062,6 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta
|
||||
|
||||
if (bc->metrics.os == TargetOs_freestanding) {
|
||||
bc->ODIN_DEFAULT_TO_NIL_ALLOCATOR = !bc->ODIN_DEFAULT_TO_PANIC_ALLOCATOR;
|
||||
} else if (is_arch_wasm()) {
|
||||
if (bc->metrics.os == TargetOs_js || bc->metrics.os == TargetOs_wasi) {
|
||||
// TODO(bill): Should these even have a default "heap-like" allocator?
|
||||
}
|
||||
|
||||
if (!bc->ODIN_DEFAULT_TO_NIL_ALLOCATOR && !bc->ODIN_DEFAULT_TO_PANIC_ALLOCATOR) {
|
||||
bc->ODIN_DEFAULT_TO_PANIC_ALLOCATOR = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-1
@@ -1092,7 +1092,13 @@ gb_internal bool cache_load_file_directive(CheckerContext *c, Ast *call, String
|
||||
|
||||
BlockingMutex *ignore_mutex = nullptr;
|
||||
bool ok = determine_path_from_string(ignore_mutex, call, base_dir, original_string, &path);
|
||||
gb_unused(ok);
|
||||
if (!ok) {
|
||||
if (err_on_not_found) {
|
||||
error(ce->proc, "Failed to `#%.*s` file: %.*s; invalid file or cannot be found", LIT(builtin_name), LIT(original_string));
|
||||
}
|
||||
call->state_flags |= StateFlag_DirectiveWasFalse;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
MUTEX_GUARD(&c->info->load_file_mutex);
|
||||
@@ -5235,6 +5241,34 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As
|
||||
operand->type = t_untyped_bool;
|
||||
break;
|
||||
|
||||
|
||||
case BuiltinProc_type_is_matrix_row_major:
|
||||
case BuiltinProc_type_is_matrix_column_major:
|
||||
{
|
||||
Operand op = {};
|
||||
Type *bt = check_type(c, ce->args[0]);
|
||||
Type *type = base_type(bt);
|
||||
if (type == nullptr || type == t_invalid) {
|
||||
error(ce->args[0], "Expected a type for '%.*s'", LIT(builtin_name));
|
||||
return false;
|
||||
}
|
||||
if (type->kind != Type_Matrix) {
|
||||
gbString s = type_to_string(bt);
|
||||
error(ce->args[0], "Expected a matrix type for '%.*s', got '%s'", LIT(builtin_name), s);
|
||||
gb_string_free(s);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id == BuiltinProc_type_is_matrix_row_major) {
|
||||
operand->value = exact_value_bool(bt->Matrix.is_row_major == true);
|
||||
} else {
|
||||
operand->value = exact_value_bool(bt->Matrix.is_row_major == false);
|
||||
}
|
||||
operand->mode = Addressing_Constant;
|
||||
operand->type = t_untyped_bool;
|
||||
break;
|
||||
}
|
||||
|
||||
case BuiltinProc_type_has_field:
|
||||
{
|
||||
Operand op = {};
|
||||
|
||||
+55
-6
@@ -1179,15 +1179,59 @@ gb_internal void check_assignment(CheckerContext *c, Operand *operand, Type *typ
|
||||
LIT(context_name));
|
||||
check_assignment_error_suggestion(c, operand, type);
|
||||
|
||||
Type *src = base_type(operand->type);
|
||||
Type *dst = base_type(type);
|
||||
if (context_name == "procedure argument") {
|
||||
Type *src = base_type(operand->type);
|
||||
Type *dst = base_type(type);
|
||||
if (is_type_slice(src) && are_types_identical(src->Slice.elem, dst)) {
|
||||
gbString a = expr_to_string(operand->expr);
|
||||
error_line("\tSuggestion: Did you mean to pass the slice into the variadic parameter with ..%s?\n\n", a);
|
||||
gb_string_free(a);
|
||||
}
|
||||
}
|
||||
if (src->kind == dst->kind && src->kind == Type_Proc) {
|
||||
Type *x = src;
|
||||
Type *y = dst;
|
||||
bool same_inputs = are_types_identical_internal(x->Proc.params, y->Proc.params, false);
|
||||
bool same_outputs = are_types_identical_internal(x->Proc.results, y->Proc.results, false);
|
||||
if (same_inputs && same_outputs &&
|
||||
x->Proc.calling_convention != y->Proc.calling_convention) {
|
||||
gbString s_expected = type_to_string(y);
|
||||
gbString s_got = type_to_string(x);
|
||||
|
||||
error_line("\tNote: The calling conventions differ between the procedure signature types\n");
|
||||
error_line("\t Expected \"%s\", got \"%s\"\n",
|
||||
proc_calling_convention_strings[y->Proc.calling_convention],
|
||||
proc_calling_convention_strings[x->Proc.calling_convention]);
|
||||
error_line("\t Expected: %s\n", s_expected);
|
||||
error_line("\t Got: %s\n", s_got);
|
||||
gb_string_free(s_got);
|
||||
gb_string_free(s_expected);
|
||||
} else if (same_inputs && !same_outputs) {
|
||||
gbString s_expected = type_to_string(y->Proc.results);
|
||||
gbString s_got = type_to_string(x->Proc.results);
|
||||
error_line("\tNote: The return types differ between the procedure signature types\n");
|
||||
error_line("\t Expected: %s\n", s_expected);
|
||||
error_line("\t Got: %s\n", s_got);
|
||||
gb_string_free(s_got);
|
||||
gb_string_free(s_expected);
|
||||
} else if (!same_inputs && same_outputs) {
|
||||
gbString s_expected = type_to_string(y->Proc.params);
|
||||
gbString s_got = type_to_string(x->Proc.params);
|
||||
error_line("\tNote: The input parameter types differ between the procedure signature types\n");
|
||||
error_line("\t Expected: %s\n", s_expected);
|
||||
error_line("\t Got: %s\n", s_got);
|
||||
gb_string_free(s_got);
|
||||
gb_string_free(s_expected);
|
||||
} else {
|
||||
gbString s_expected = type_to_string(y);
|
||||
gbString s_got = type_to_string(x);
|
||||
error_line("\tNote: The signature type do not match whatsoever\n");
|
||||
error_line("\t Expected: %s\n", s_expected);
|
||||
error_line("\t Got: %s\n", s_got);
|
||||
gb_string_free(s_got);
|
||||
gb_string_free(s_expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1761,7 +1805,7 @@ gb_internal Entity *check_ident(CheckerContext *c, Operand *o, Ast *n, Type *nam
|
||||
|
||||
case Entity_ImportName:
|
||||
if (!allow_import_name) {
|
||||
error(n, "Use of import '%.*s' not in selector", LIT(name));
|
||||
error(n, "Use of import name '%.*s' not in the form of 'x.y'", LIT(name));
|
||||
}
|
||||
return e;
|
||||
case Entity_LibraryName:
|
||||
@@ -7757,13 +7801,18 @@ gb_internal bool check_set_index_data(Operand *o, Type *t, bool indirection, i64
|
||||
return true;
|
||||
|
||||
case Type_Matrix:
|
||||
*max_count = t->Matrix.column_count;
|
||||
if (indirection) {
|
||||
o->mode = Addressing_Variable;
|
||||
} else if (o->mode != Addressing_Variable) {
|
||||
o->mode = Addressing_Value;
|
||||
}
|
||||
o->type = alloc_type_array(t->Matrix.elem, t->Matrix.row_count);
|
||||
if (t->Matrix.is_row_major) {
|
||||
*max_count = t->Matrix.row_count;
|
||||
o->type = alloc_type_array(t->Matrix.elem, t->Matrix.column_count);
|
||||
} else {
|
||||
*max_count = t->Matrix.column_count;
|
||||
o->type = alloc_type_array(t->Matrix.elem, t->Matrix.row_count);
|
||||
}
|
||||
return true;
|
||||
|
||||
case Type_Slice:
|
||||
@@ -10159,7 +10208,7 @@ gb_internal ExprKind check_index_expr(CheckerContext *c, Operand *o, Ast *node,
|
||||
o->mode = Addressing_Invalid;
|
||||
o->expr = node;
|
||||
return kind;
|
||||
} else if (ok) {
|
||||
} else if (ok && !is_type_matrix(t)) {
|
||||
ExactValue value = type_and_value_of_expr(ie->expr).value;
|
||||
o->mode = Addressing_Constant;
|
||||
bool success = false;
|
||||
|
||||
+7
-2
@@ -565,7 +565,11 @@ gb_internal Type *check_assignment_variable(CheckerContext *ctx, Operand *lhs, O
|
||||
} else {
|
||||
error(lhs->expr, "Cannot assign to '%s' which is a procedure parameter", str);
|
||||
}
|
||||
error_line("\tSuggestion: Did you mean to pass '%.*s' by pointer?\n", LIT(e->token.string));
|
||||
if (is_type_pointer(e->type)) {
|
||||
error_line("\tSuggestion: Did you mean to shadow it? '%.*s := %.*s'?\n", LIT(e->token.string), LIT(e->token.string));
|
||||
} else {
|
||||
error_line("\tSuggestion: Did you mean to pass '%.*s' by pointer?\n", LIT(e->token.string));
|
||||
}
|
||||
show_error_on_line(e->token.pos, token_pos_end(e->token));
|
||||
} else {
|
||||
ERROR_BLOCK();
|
||||
@@ -1663,6 +1667,7 @@ gb_internal void check_range_stmt(CheckerContext *ctx, Ast *node, u32 mod_flags)
|
||||
}
|
||||
}
|
||||
}
|
||||
bool is_ptr = type_deref(operand.type);
|
||||
Type *t = base_type(type_deref(operand.type));
|
||||
|
||||
switch (t->kind) {
|
||||
@@ -1707,7 +1712,7 @@ gb_internal void check_range_stmt(CheckerContext *ctx, Ast *node, u32 mod_flags)
|
||||
break;
|
||||
|
||||
case Type_Array:
|
||||
is_possibly_addressable = operand.mode == Addressing_Variable;
|
||||
is_possibly_addressable = operand.mode == Addressing_Variable || is_ptr;
|
||||
array_add(&vals, t->Array.elem);
|
||||
array_add(&vals, t_int);
|
||||
break;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -256,6 +256,9 @@ BuiltinProc__type_simple_boolean_begin,
|
||||
|
||||
BuiltinProc__type_simple_boolean_end,
|
||||
|
||||
BuiltinProc_type_is_matrix_row_major,
|
||||
BuiltinProc_type_is_matrix_column_major,
|
||||
|
||||
BuiltinProc_type_has_field,
|
||||
BuiltinProc_type_field_type,
|
||||
|
||||
@@ -567,6 +570,9 @@ gb_global BuiltinProc builtin_procs[BuiltinProc_COUNT] = {
|
||||
{STR_LIT("type_has_nil"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics},
|
||||
{STR_LIT(""), 0, false, Expr_Stmt, BuiltinProcPkg_intrinsics},
|
||||
|
||||
{STR_LIT("type_is_matrix_row_major"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics},
|
||||
{STR_LIT("type_is_matrix_column_major"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics},
|
||||
|
||||
{STR_LIT("type_has_field"), 2, false, Expr_Expr, BuiltinProcPkg_intrinsics},
|
||||
{STR_LIT("type_field_type"), 2, false, Expr_Expr, BuiltinProcPkg_intrinsics},
|
||||
|
||||
|
||||
+4
-3
@@ -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)
|
||||
|
||||
@@ -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, "}");
|
||||
|
||||
@@ -2223,8 +2223,8 @@ gb_internal LLVMAtomicOrdering llvm_atomic_ordering_from_odin(ExactValue const &
|
||||
GB_ASSERT(value.kind == ExactValue_Integer);
|
||||
i64 v = exact_value_to_i64(value);
|
||||
switch (v) {
|
||||
case OdinAtomicMemoryOrder_relaxed: return LLVMAtomicOrderingUnordered;
|
||||
case OdinAtomicMemoryOrder_consume: return LLVMAtomicOrderingMonotonic;
|
||||
case OdinAtomicMemoryOrder_relaxed: return LLVMAtomicOrderingMonotonic;
|
||||
case OdinAtomicMemoryOrder_consume: return LLVMAtomicOrderingAcquire;
|
||||
case OdinAtomicMemoryOrder_acquire: return LLVMAtomicOrderingAcquire;
|
||||
case OdinAtomicMemoryOrder_release: return LLVMAtomicOrderingRelease;
|
||||
case OdinAtomicMemoryOrder_acq_rel: return LLVMAtomicOrderingAcquireRelease;
|
||||
|
||||
+8
-2
@@ -3499,8 +3499,14 @@ gb_internal Array<Ast *> parse_ident_list(AstFile *f, bool allow_poly_names) {
|
||||
gb_internal Ast *parse_type(AstFile *f) {
|
||||
Ast *type = parse_type_or_ident(f);
|
||||
if (type == nullptr) {
|
||||
Token token = advance_token(f);
|
||||
syntax_error(token, "Expected a type");
|
||||
Token prev_token = f->curr_token;
|
||||
Token token = {};
|
||||
if (f->curr_token.kind == Token_OpenBrace) {
|
||||
token = f->curr_token;
|
||||
} else {
|
||||
token = advance_token(f);
|
||||
}
|
||||
syntax_error(token, "Expected a type, got '%.*s'", LIT(prev_token.string));
|
||||
return ast_bad_expr(f, token, f->curr_token);
|
||||
} else if (type->kind == Ast_ParenExpr &&
|
||||
unparen_expr(type) == nullptr) {
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.1.0\n"
|
||||
"PO-Revision-Date: 2024-04-13 11:13+0200\n"
|
||||
"Last-Translator: Someone <someone@somewhere.com>\n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_IE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgctxt "Context"
|
||||
msgid "Message1"
|
||||
msgstr "This is message 1 with Context"
|
||||
|
||||
msgid "Message1"
|
||||
msgstr "This is message 1 without Context"
|
||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.1.0\n"
|
||||
"PO-Revision-Date: 2024-04-13 11:13+0200\n"
|
||||
"Last-Translator: Someone <someone@somewhere.com>\n"
|
||||
"Language-Team: English\n"
|
||||
"Language: it_IT\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2);\n"
|
||||
|
||||
msgid "Message1"
|
||||
msgid_plural "Message1/plural"
|
||||
msgstr[0] "This is message 1"
|
||||
msgstr[1] "This is message 1 - plural A"
|
||||
msgstr[2] "This is message 1 - plural B"
|
||||
+40
-45
@@ -3,30 +3,15 @@ set COMMON=-no-bounds-check -vet -strict-style
|
||||
set COLLECTION=-collection:tests=..
|
||||
set PATH_TO_ODIN==..\..\odin
|
||||
python3 download_assets.py
|
||||
echo ---
|
||||
echo Running core:image tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run image %COMMON% -out:test_core_image.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:compress tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run compress %COMMON% -out:test_core_compress.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:strings tests
|
||||
echo Running core:container tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run strings %COMMON% -out:test_core_strings.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:hash tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run hash %COMMON% -o:size -out:test_core_hash.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:odin tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run odin %COMMON% -o:size -out:test_core_odin.exe || exit /b
|
||||
%PATH_TO_ODIN% run container %COMMON% %COLLECTION% -out:test_core_container.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:crypto tests
|
||||
@@ -45,9 +30,19 @@ rem %PATH_TO_ODIN% run encoding/hxa %COMMON% %COLLECTION% -out:test_hxa.exe |
|
||||
%PATH_TO_ODIN% run encoding/base64 %COMMON% -out:test_base64.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:math/noise tests
|
||||
echo Running core:fmt tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run math/noise %COMMON% -out:test_noise.exe || exit /b
|
||||
%PATH_TO_ODIN% run fmt %COMMON% %COLLECTION% -out:test_core_fmt.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:hash tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run hash %COMMON% -o:size -out:test_core_hash.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:image tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run image %COMMON% -out:test_core_image.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:math tests
|
||||
@@ -59,6 +54,21 @@ echo Running core:math/linalg/glsl tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run math/linalg/glsl %COMMON% %COLLECTION% -out:test_linalg_glsl.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:math/noise tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run math/noise %COMMON% -out:test_noise.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:net
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run net %COMMON% -out:test_core_net.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:odin tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run odin %COMMON% -o:size -out:test_core_odin.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:path/filepath tests
|
||||
echo ---
|
||||
@@ -69,47 +79,32 @@ echo Running core:reflect tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run reflect %COMMON% %COLLECTION% -out:test_core_reflect.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:runtime tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run runtime %COMMON% %COLLECTION% -out:test_core_runtime.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:slice tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run slice %COMMON% -out:test_core_slice.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:strings tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run strings %COMMON% -out:test_core_strings.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:text/i18n tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run text\i18n %COMMON% -out:test_core_i18n.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:net
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run net %COMMON% -out:test_core_net.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:slice tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run slice %COMMON% -out:test_core_slice.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:container tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run container %COMMON% %COLLECTION% -out:test_core_container.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:thread tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run thread %COMMON% %COLLECTION% -out:test_core_thread.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:runtime tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run runtime %COMMON% %COLLECTION% -out:test_core_runtime.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:time tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run time %COMMON% %COLLECTION% -out:test_core_time.exe || exit /b
|
||||
|
||||
echo ---
|
||||
echo Running core:fmt tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run fmt %COMMON% %COLLECTION% -out:test_core_fmt.exe || exit /b
|
||||
%PATH_TO_ODIN% run time %COMMON% %COLLECTION% -out:test_core_time.exe || exit /b
|
||||
@@ -4,12 +4,12 @@ import "core:container/avl"
|
||||
import "core:math/rand"
|
||||
import "core:slice"
|
||||
import "core:testing"
|
||||
|
||||
import "core:fmt"
|
||||
import tc "tests:common"
|
||||
|
||||
@(test)
|
||||
test_avl :: proc(t: ^testing.T) {
|
||||
tc.log(t, "Testing avl")
|
||||
tc.log(t, fmt.tprintf("Testing avl, using random seed %v, add -define:RANDOM_SEED=%v to reuse it.", random_seed, random_seed))
|
||||
|
||||
// Initialization.
|
||||
tree: avl.Tree(int)
|
||||
@@ -21,11 +21,14 @@ test_avl :: proc(t: ^testing.T) {
|
||||
iter := avl.iterator(&tree, avl.Direction.Forward)
|
||||
tc.expect(t, avl.iterator_get(&iter) == nil, "empty/iterator: first node should be nil")
|
||||
|
||||
r: rand.Rand
|
||||
rand.init(&r, random_seed)
|
||||
|
||||
// Test insertion.
|
||||
NR_INSERTS :: 32 + 1 // Ensure at least 1 collision.
|
||||
inserted_map := make(map[int]^avl.Node(int))
|
||||
for i := 0; i < NR_INSERTS; i += 1 {
|
||||
v := int(rand.uint32() & 0x1f)
|
||||
v := int(rand.uint32(&r) & 0x1f)
|
||||
existing_node, in_map := inserted_map[v]
|
||||
|
||||
n, ok, _ := avl.find_or_insert(&tree, v)
|
||||
@@ -38,7 +41,7 @@ test_avl :: proc(t: ^testing.T) {
|
||||
}
|
||||
nrEntries := len(inserted_map)
|
||||
tc.expect(t, avl.len(&tree) == nrEntries, "insert: len after")
|
||||
tree_validate(t, &tree)
|
||||
validate_avl(t, &tree)
|
||||
|
||||
// Ensure that all entries can be found.
|
||||
for k, v in inserted_map {
|
||||
@@ -74,7 +77,7 @@ test_avl :: proc(t: ^testing.T) {
|
||||
tc.expect(t, visited == nrEntries, "iterator/backward: visited")
|
||||
|
||||
// Test removal.
|
||||
rand.shuffle(inserted_values[:])
|
||||
rand.shuffle(inserted_values[:], &r)
|
||||
for v, i in inserted_values {
|
||||
node := avl.find(&tree, v)
|
||||
tc.expect(t, node != nil, "remove: find (pre)")
|
||||
@@ -82,7 +85,7 @@ test_avl :: proc(t: ^testing.T) {
|
||||
ok := avl.remove(&tree, v)
|
||||
tc.expect(t, ok, "remove: succeeds")
|
||||
tc.expect(t, nrEntries - (i + 1) == avl.len(&tree), "remove: len (post)")
|
||||
tree_validate(t, &tree)
|
||||
validate_avl(t, &tree)
|
||||
|
||||
tc.expect(t, nil == avl.find(&tree, v), "remove: find (post")
|
||||
}
|
||||
@@ -114,7 +117,7 @@ test_avl :: proc(t: ^testing.T) {
|
||||
tc.expect(t, ok == (avl.len(&tree) > 0), "iterator/remove: next should return false")
|
||||
tc.expect(t, node == avl.first(&tree), "iterator/remove: next should return first")
|
||||
|
||||
tree_validate(t, &tree)
|
||||
validate_avl(t, &tree)
|
||||
}
|
||||
tc.expect(t, avl.len(&tree) == nrEntries - 1, "iterator/remove: len should drop by 1")
|
||||
|
||||
@@ -123,7 +126,7 @@ test_avl :: proc(t: ^testing.T) {
|
||||
}
|
||||
|
||||
@(private)
|
||||
tree_validate :: proc(t: ^testing.T, tree: ^avl.Tree($Value)) {
|
||||
validate_avl :: proc(t: ^testing.T, tree: ^avl.Tree($Value)) {
|
||||
tree_check_invariants(t, tree, tree._root, nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ main :: proc() {
|
||||
t := testing.T{}
|
||||
|
||||
test_avl(&t)
|
||||
test_rbtree(&t)
|
||||
test_small_array(&t)
|
||||
|
||||
tc.report(&t)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
package test_core_container
|
||||
|
||||
import rb "core:container/rbtree"
|
||||
import "core:math/rand"
|
||||
import "core:testing"
|
||||
import "core:fmt"
|
||||
import "base:intrinsics"
|
||||
import "core:mem"
|
||||
import "core:slice"
|
||||
import tc "tests:common"
|
||||
|
||||
RANDOM_SEED :: #config(RANDOM_SEED, 0)
|
||||
random_seed := u64(intrinsics.read_cycle_counter()) when RANDOM_SEED == 0 else u64(RANDOM_SEED)
|
||||
|
||||
test_rbtree_integer :: proc(t: ^testing.T, $Key: typeid, $Value: typeid) {
|
||||
track: mem.Tracking_Allocator
|
||||
mem.tracking_allocator_init(&track, context.allocator)
|
||||
defer mem.tracking_allocator_destroy(&track)
|
||||
context.allocator = mem.tracking_allocator(&track)
|
||||
|
||||
r: rand.Rand
|
||||
rand.init(&r, random_seed)
|
||||
|
||||
tc.log(t, fmt.tprintf("Testing Red-Black Tree($Key=%v,$Value=%v), using random seed %v, add -define:RANDOM_SEED=%v to reuse it.", type_info_of(Key), type_info_of(Value), random_seed, random_seed))
|
||||
tree: rb.Tree(Key, Value)
|
||||
rb.init(&tree)
|
||||
|
||||
tc.expect(t, rb.len(&tree) == 0, "empty: len should be 0")
|
||||
tc.expect(t, rb.first(&tree) == nil, "empty: first should be nil")
|
||||
tc.expect(t, rb.last(&tree) == nil, "empty: last should be nil")
|
||||
iter := rb.iterator(&tree, .Forward)
|
||||
tc.expect(t, rb.iterator_get(&iter) == nil, "empty/iterator: first node should be nil")
|
||||
|
||||
// Test insertion.
|
||||
NR_INSERTS :: 32 + 1 // Ensure at least 1 collision.
|
||||
inserted_map := make(map[Key]^rb.Node(Key, Value))
|
||||
|
||||
min_key := max(Key)
|
||||
max_key := min(Key)
|
||||
|
||||
for i := 0; i < NR_INSERTS; i += 1 {
|
||||
k := Key(rand.uint32(&r)) & 0x1f
|
||||
min_key = min(min_key, k); max_key = max(max_key, k)
|
||||
v := Value(rand.uint32(&r))
|
||||
|
||||
existing_node, in_map := inserted_map[k]
|
||||
n, inserted, _ := rb.find_or_insert(&tree, k, v)
|
||||
tc.expect(t, in_map != inserted, "insert: inserted should match inverse of map lookup")
|
||||
if inserted {
|
||||
inserted_map[k] = n
|
||||
} else {
|
||||
tc.expect(t, existing_node == n, "insert: expecting existing node")
|
||||
}
|
||||
}
|
||||
|
||||
entry_count := len(inserted_map)
|
||||
tc.expect(t, rb.len(&tree) == entry_count, "insert: len after")
|
||||
validate_rbtree(t, &tree)
|
||||
|
||||
first := rb.first(&tree)
|
||||
last := rb.last(&tree)
|
||||
tc.expect(t, first != nil && first.key == min_key, fmt.tprintf("insert: first should be present with key %v", min_key))
|
||||
tc.expect(t, last != nil && last.key == max_key, fmt.tprintf("insert: last should be present with key %v", max_key))
|
||||
|
||||
// Ensure that all entries can be found.
|
||||
for k, v in inserted_map {
|
||||
tc.expect(t, v == rb.find(&tree, k), "Find(): Node")
|
||||
tc.expect(t, k == v.key, "Find(): Node key")
|
||||
}
|
||||
|
||||
// Test the forward/backward iterators.
|
||||
inserted_keys: [dynamic]Key
|
||||
for k in inserted_map {
|
||||
append(&inserted_keys, k)
|
||||
}
|
||||
slice.sort(inserted_keys[:])
|
||||
|
||||
iter = rb.iterator(&tree, rb.Direction.Forward)
|
||||
visited: int
|
||||
for node in rb.iterator_next(&iter) {
|
||||
k, idx := node.key, visited
|
||||
tc.expect(t, inserted_keys[idx] == k, "iterator/forward: key")
|
||||
tc.expect(t, node == rb.iterator_get(&iter), "iterator/forward: get")
|
||||
visited += 1
|
||||
}
|
||||
tc.expect(t, visited == entry_count, "iterator/forward: visited")
|
||||
|
||||
slice.reverse(inserted_keys[:])
|
||||
iter = rb.iterator(&tree, rb.Direction.Backward)
|
||||
visited = 0
|
||||
for node in rb.iterator_next(&iter) {
|
||||
k, idx := node.key, visited
|
||||
tc.expect(t, inserted_keys[idx] == k, "iterator/backward: key")
|
||||
visited += 1
|
||||
}
|
||||
tc.expect(t, visited == entry_count, "iterator/backward: visited")
|
||||
|
||||
// Test removal (and on_remove callback)
|
||||
rand.shuffle(inserted_keys[:], &r)
|
||||
callback_count := entry_count
|
||||
tree.user_data = &callback_count
|
||||
tree.on_remove = proc(key: Key, value: Value, user_data: rawptr) {
|
||||
(^int)(user_data)^ -= 1
|
||||
}
|
||||
for k, i in inserted_keys {
|
||||
node := rb.find(&tree, k)
|
||||
tc.expect(t, node != nil, "remove: find (pre)")
|
||||
|
||||
ok := rb.remove(&tree, k)
|
||||
tc.expect(t, ok, "remove: succeeds")
|
||||
tc.expect(t, entry_count - (i + 1) == rb.len(&tree), "remove: len (post)")
|
||||
validate_rbtree(t, &tree)
|
||||
|
||||
tc.expect(t, nil == rb.find(&tree, k), "remove: find (post")
|
||||
}
|
||||
tc.expect(t, rb.len(&tree) == 0, "remove: len should be 0")
|
||||
tc.expect(t, callback_count == 0, fmt.tprintf("remove: on_remove should've been called %v times, it was %v", entry_count, callback_count))
|
||||
tc.expect(t, rb.first(&tree) == nil, "remove: first should be nil")
|
||||
tc.expect(t, rb.last(&tree) == nil, "remove: last should be nil")
|
||||
|
||||
// Refill the tree.
|
||||
for k in inserted_keys {
|
||||
rb.find_or_insert(&tree, k, 42)
|
||||
}
|
||||
|
||||
// Test that removing the node doesn't break the iterator.
|
||||
callback_count = entry_count
|
||||
iter = rb.iterator(&tree, rb.Direction.Forward)
|
||||
if node := rb.iterator_get(&iter); node != nil {
|
||||
k := node.key
|
||||
|
||||
ok := rb.iterator_remove(&iter)
|
||||
tc.expect(t, ok, "iterator/remove: success")
|
||||
|
||||
ok = rb.iterator_remove(&iter)
|
||||
tc.expect(t, !ok, "iterator/remove: redundant removes should fail")
|
||||
|
||||
tc.expect(t, rb.find(&tree, k) == nil, "iterator/remove: node should be gone")
|
||||
tc.expect(t, rb.iterator_get(&iter) == nil, "iterator/remove: get should return nil")
|
||||
|
||||
// Ensure that iterator_next still works.
|
||||
node, ok = rb.iterator_next(&iter)
|
||||
tc.expect(t, ok == (rb.len(&tree) > 0), "iterator/remove: next should return false")
|
||||
tc.expect(t, node == rb.first(&tree), "iterator/remove: next should return first")
|
||||
|
||||
validate_rbtree(t, &tree)
|
||||
}
|
||||
tc.expect(t, rb.len(&tree) == entry_count - 1, "iterator/remove: len should drop by 1")
|
||||
|
||||
rb.destroy(&tree)
|
||||
tc.expect(t, rb.len(&tree) == 0, "destroy: len should be 0")
|
||||
tc.expect(t, callback_count == 0, fmt.tprintf("remove: on_remove should've been called %v times, it was %v", entry_count, callback_count))
|
||||
|
||||
// print_tree_node(tree._root)
|
||||
delete(inserted_map)
|
||||
delete(inserted_keys)
|
||||
tc.expect(t, len(track.allocation_map) == 0, fmt.tprintf("Expected 0 leaks, have %v", len(track.allocation_map)))
|
||||
tc.expect(t, len(track.bad_free_array) == 0, fmt.tprintf("Expected 0 bad frees, have %v", len(track.bad_free_array)))
|
||||
return
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_rbtree :: proc(t: ^testing.T) {
|
||||
test_rbtree_integer(t, u16, u16)
|
||||
}
|
||||
|
||||
print_tree_node :: proc(n: ^$N/rb.Node($Key, $Value), indent := 0) {
|
||||
if n == nil {
|
||||
fmt.println("<empty tree>")
|
||||
return
|
||||
}
|
||||
if n.right != nil {
|
||||
print_tree_node(n.right, indent + 1)
|
||||
}
|
||||
for _ in 0..<indent {
|
||||
fmt.printf("\t")
|
||||
}
|
||||
if n.color == .Black {
|
||||
fmt.printfln("%v", n.key)
|
||||
} else {
|
||||
fmt.printfln("<%v>", n.key)
|
||||
}
|
||||
if n.left != nil {
|
||||
print_tree_node(n.left, indent + 1)
|
||||
}
|
||||
}
|
||||
|
||||
validate_rbtree :: proc(t: ^testing.T, tree: ^$T/rb.Tree($Key, $Value)) {
|
||||
verify_rbtree_propery_1(t, tree._root)
|
||||
verify_rbtree_propery_2(t, tree._root)
|
||||
/* Property 3 is implicit */
|
||||
verify_rbtree_propery_4(t, tree._root)
|
||||
verify_rbtree_propery_5(t, tree._root)
|
||||
}
|
||||
|
||||
verify_rbtree_propery_1 :: proc(t: ^testing.T, n: ^$N/rb.Node($Key, $Value)) {
|
||||
tc.expect(t, rb.node_color(n) == .Black || rb.node_color(n) == .Red, "Property #1: Each node is either red or black.")
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
verify_rbtree_propery_1(t, n._left)
|
||||
verify_rbtree_propery_1(t, n._right)
|
||||
}
|
||||
|
||||
verify_rbtree_propery_2 :: proc(t: ^testing.T, root: ^$N/rb.Node($Key, $Value)) {
|
||||
tc.expect(t, rb.node_color(root) == .Black, "Property #2: Root node should be black.")
|
||||
}
|
||||
|
||||
verify_rbtree_propery_4 :: proc(t: ^testing.T, n: ^$N/rb.Node($Key, $Value)) {
|
||||
if rb.node_color(n) == .Red {
|
||||
// A red node's left, right and parent should be black
|
||||
all_black := rb.node_color(n._left) == .Black && rb.node_color(n._right) == .Black && rb.node_color(n._parent) == .Black
|
||||
tc.expect(t, all_black, "Property #3: Red node's children + parent must be black.")
|
||||
}
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
verify_rbtree_propery_4(t, n._left)
|
||||
verify_rbtree_propery_4(t, n._right)
|
||||
}
|
||||
|
||||
verify_rbtree_propery_5 :: proc(t: ^testing.T, root: ^$N/rb.Node($Key, $Value)) {
|
||||
black_count_path := -1
|
||||
verify_rbtree_propery_5_helper(t, root, 0, &black_count_path)
|
||||
}
|
||||
verify_rbtree_propery_5_helper :: proc(t: ^testing.T, n: ^$N/rb.Node($Key, $Value), black_count: int, path_black_count: ^int) {
|
||||
black_count := black_count
|
||||
|
||||
if rb.node_color(n) == .Black {
|
||||
black_count += 1
|
||||
}
|
||||
if n == nil {
|
||||
if path_black_count^ == -1 {
|
||||
path_black_count^ = black_count
|
||||
} else {
|
||||
tc.expect(t, black_count == path_black_count^, "Property #5: Paths from a node to its leaves contain same black count.")
|
||||
}
|
||||
return
|
||||
}
|
||||
verify_rbtree_propery_5_helper(t, n._left, black_count, path_black_count)
|
||||
verify_rbtree_propery_5_helper(t, n._right, black_count, path_black_count)
|
||||
}
|
||||
// Properties 4 and 5 together guarantee that no path in the tree is more than about twice as long as any other path,
|
||||
// which guarantees that it has O(log n) height.
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+137
-23
@@ -29,6 +29,7 @@ import "core:sort"
|
||||
import "core:strings"
|
||||
import "core:strconv"
|
||||
import "core:math"
|
||||
import textedit "core:text/edit"
|
||||
|
||||
COMMAND_LIST_SIZE :: #config(MICROUI_COMMAND_LIST_SIZE, 256 * 1024)
|
||||
ROOT_LIST_SIZE :: #config(MICROUI_ROOT_LIST_SIZE, 32)
|
||||
@@ -51,6 +52,7 @@ Clip :: enum u32 {
|
||||
|
||||
Color_Type :: enum u32 {
|
||||
TEXT,
|
||||
SELECTION_BG,
|
||||
BORDER,
|
||||
WINDOW_BG,
|
||||
TITLE_BG,
|
||||
@@ -111,7 +113,16 @@ Key :: enum u32 {
|
||||
CTRL,
|
||||
ALT,
|
||||
BACKSPACE,
|
||||
DELETE,
|
||||
RETURN,
|
||||
LEFT,
|
||||
RIGHT,
|
||||
HOME,
|
||||
END,
|
||||
A,
|
||||
X,
|
||||
C,
|
||||
V,
|
||||
}
|
||||
Key_Set :: distinct bit_set[Key; u32]
|
||||
|
||||
@@ -235,6 +246,8 @@ Context :: struct {
|
||||
key_down_bits, key_pressed_bits: Key_Set,
|
||||
_text_store: [MAX_TEXT_STORE]u8,
|
||||
text_input: strings.Builder, // uses `_text_store` as backing store with nil_allocator.
|
||||
textbox_state: textedit.State,
|
||||
textbox_offset: i32,
|
||||
}
|
||||
|
||||
Stack :: struct($T: typeid, $N: int) {
|
||||
@@ -260,6 +273,7 @@ default_style := Style{
|
||||
scrollbar_size = 12, thumb_size = 8,
|
||||
colors = {
|
||||
.TEXT = {230, 230, 230, 255},
|
||||
.SELECTION_BG = {90, 90, 90, 255},
|
||||
.BORDER = {25, 25, 25, 255},
|
||||
.WINDOW_BG = {50, 50, 50, 255},
|
||||
.TITLE_BG = {25, 25, 25, 255},
|
||||
@@ -305,12 +319,16 @@ default_draw_frame :: proc(ctx: ^Context, rect: Rect, colorid: Color_Type) {
|
||||
}
|
||||
}
|
||||
|
||||
init :: proc(ctx: ^Context) {
|
||||
init :: proc(ctx: ^Context, set_clipboard: proc(user_data: rawptr, text: string) -> (ok: bool), get_clipboard: proc(user_data: rawptr) -> (text: string, ok: bool), clipboard_user_data: rawptr) {
|
||||
ctx^ = {} // zero memory
|
||||
ctx.draw_frame = default_draw_frame
|
||||
ctx._style = default_style
|
||||
ctx.style = &ctx._style
|
||||
ctx.text_input = strings.builder_from_bytes(ctx._text_store[:])
|
||||
|
||||
ctx.textbox_state.set_clipboard = set_clipboard
|
||||
ctx.textbox_state.get_clipboard = get_clipboard
|
||||
ctx.textbox_state.clipboard_user_data = clipboard_user_data
|
||||
}
|
||||
|
||||
begin :: proc(ctx: ^Context) {
|
||||
@@ -967,23 +985,95 @@ checkbox :: proc(ctx: ^Context, label: string, state: ^bool) -> (res: Result_Set
|
||||
textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect, opt := Options{}) -> (res: Result_Set) {
|
||||
update_control(ctx, id, r, opt | {.HOLD_FOCUS})
|
||||
|
||||
font := ctx.style.font
|
||||
|
||||
if ctx.focus_id == id {
|
||||
/* create a builder backed by the user's buffer */
|
||||
builder := strings.builder_from_bytes(textbuf)
|
||||
non_zero_resize(&builder.buf, textlen^)
|
||||
ctx.textbox_state.builder = &builder
|
||||
if ctx.textbox_state.id != u64(id) {
|
||||
ctx.textbox_state.id = u64(id)
|
||||
ctx.textbox_state.selection = {}
|
||||
}
|
||||
|
||||
/* check selection bounds */
|
||||
if ctx.textbox_state.selection[0] > textlen^ || ctx.textbox_state.selection[1] > textlen^ {
|
||||
ctx.textbox_state.selection = {}
|
||||
}
|
||||
|
||||
/* handle text input */
|
||||
n := min(len(textbuf) - textlen^, strings.builder_len(ctx.text_input))
|
||||
if n > 0 {
|
||||
copy(textbuf[textlen^:], strings.to_string(ctx.text_input)[:n])
|
||||
textlen^ += n
|
||||
if strings.builder_len(ctx.text_input) > 0 {
|
||||
if textedit.input_text(&ctx.textbox_state, strings.to_string(ctx.text_input)) > 0 {
|
||||
textlen^ = strings.builder_len(builder)
|
||||
res += {.CHANGE}
|
||||
}
|
||||
}
|
||||
/* handle ctrl+a */
|
||||
if .A in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits {
|
||||
ctx.textbox_state.selection = {textlen^, 0}
|
||||
}
|
||||
/* handle ctrl+x */
|
||||
if .X in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits {
|
||||
if textedit.cut(&ctx.textbox_state) {
|
||||
textlen^ = strings.builder_len(builder)
|
||||
res += {.CHANGE}
|
||||
}
|
||||
}
|
||||
/* handle ctrl+c */
|
||||
if .C in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits {
|
||||
textedit.copy(&ctx.textbox_state)
|
||||
}
|
||||
/* handle ctrl+v */
|
||||
if .V in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits {
|
||||
if textedit.paste(&ctx.textbox_state) {
|
||||
textlen^ = strings.builder_len(builder)
|
||||
res += {.CHANGE}
|
||||
}
|
||||
}
|
||||
/* handle left/right */
|
||||
if .LEFT in ctx.key_pressed_bits {
|
||||
move: textedit.Translation = .Word_Left if .CTRL in ctx.key_down_bits else .Left
|
||||
if .SHIFT in ctx.key_down_bits {
|
||||
textedit.select_to(&ctx.textbox_state, move)
|
||||
} else {
|
||||
textedit.move_to(&ctx.textbox_state, move)
|
||||
}
|
||||
}
|
||||
if .RIGHT in ctx.key_pressed_bits {
|
||||
move: textedit.Translation = .Word_Right if .CTRL in ctx.key_down_bits else .Right
|
||||
if .SHIFT in ctx.key_down_bits {
|
||||
textedit.select_to(&ctx.textbox_state, move)
|
||||
} else {
|
||||
textedit.move_to(&ctx.textbox_state, move)
|
||||
}
|
||||
}
|
||||
/* handle home/end */
|
||||
if .HOME in ctx.key_pressed_bits {
|
||||
if .SHIFT in ctx.key_down_bits {
|
||||
textedit.select_to(&ctx.textbox_state, .Start)
|
||||
} else {
|
||||
textedit.move_to(&ctx.textbox_state, .Start)
|
||||
}
|
||||
}
|
||||
if .END in ctx.key_pressed_bits {
|
||||
if .SHIFT in ctx.key_down_bits {
|
||||
textedit.select_to(&ctx.textbox_state, .End)
|
||||
} else {
|
||||
textedit.move_to(&ctx.textbox_state, .End)
|
||||
}
|
||||
}
|
||||
/* handle backspace/delete */
|
||||
if .BACKSPACE in ctx.key_pressed_bits && textlen^ > 0 {
|
||||
move: textedit.Translation = .Word_Left if .CTRL in ctx.key_down_bits else .Left
|
||||
textedit.delete_to(&ctx.textbox_state, move)
|
||||
textlen^ = strings.builder_len(builder)
|
||||
res += {.CHANGE}
|
||||
}
|
||||
/* handle backspace */
|
||||
if .BACKSPACE in ctx.key_pressed_bits && textlen^ > 0 {
|
||||
/* skip utf-8 continuation bytes */
|
||||
for textlen^ > 0 {
|
||||
textlen^ -= 1
|
||||
if textbuf[textlen^] & 0xc0 != 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if .DELETE in ctx.key_pressed_bits && textlen^ > 0 {
|
||||
move: textedit.Translation = .Word_Right if .CTRL in ctx.key_down_bits else .Right
|
||||
textedit.delete_to(&ctx.textbox_state, move)
|
||||
textlen^ = strings.builder_len(builder)
|
||||
res += {.CHANGE}
|
||||
}
|
||||
/* handle return */
|
||||
@@ -991,6 +1081,25 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect
|
||||
set_focus(ctx, 0)
|
||||
res += {.SUBMIT}
|
||||
}
|
||||
|
||||
/* handle click/drag */
|
||||
if .LEFT in ctx.mouse_down_bits {
|
||||
idx := textlen^
|
||||
for i in 0..<textlen^ {
|
||||
/* skip continuation bytes */
|
||||
if textbuf[i] >= 0x80 && textbuf[i] < 0xc0 {
|
||||
continue
|
||||
}
|
||||
if ctx.mouse_pos.x < r.x + ctx.textbox_offset + ctx.text_width(font, string(textbuf[:i])) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
ctx.textbox_state.selection[0] = idx
|
||||
if .LEFT in ctx.mouse_pressed_bits && .SHIFT not_in ctx.key_down_bits {
|
||||
ctx.textbox_state.selection[1] = idx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textstr := string(textbuf[:textlen^])
|
||||
@@ -998,16 +1107,21 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect
|
||||
/* draw */
|
||||
draw_control_frame(ctx, id, r, .BASE, opt)
|
||||
if ctx.focus_id == id {
|
||||
color := ctx.style.colors[.TEXT]
|
||||
font := ctx.style.font
|
||||
textw := ctx.text_width(font, textstr)
|
||||
texth := ctx.text_height(font)
|
||||
ofx := r.w - ctx.style.padding - textw - 1
|
||||
textx := r.x + min(ofx, ctx.style.padding)
|
||||
texty := r.y + (r.h - texth) / 2
|
||||
text_color := ctx.style.colors[.TEXT]
|
||||
sel_color := ctx.style.colors[.SELECTION_BG]
|
||||
textw := ctx.text_width(font, textstr)
|
||||
texth := ctx.text_height(font)
|
||||
headx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[0]])
|
||||
tailx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[1]])
|
||||
ofmin := max(ctx.style.padding - headx, r.w - textw - ctx.style.padding)
|
||||
ofmax := min(r.w - headx - ctx.style.padding, ctx.style.padding)
|
||||
ctx.textbox_offset = clamp(ctx.textbox_offset, ofmin, ofmax)
|
||||
textx := r.x + ctx.textbox_offset
|
||||
texty := r.y + (r.h - texth) / 2
|
||||
push_clip_rect(ctx, r)
|
||||
draw_text(ctx, font, textstr, Vec2{textx, texty}, color)
|
||||
draw_rect(ctx, Rect{textx + textw, texty, 1, texth}, color)
|
||||
draw_rect(ctx, Rect{textx + min(headx, tailx), texty, abs(headx - tailx), texth}, sel_color)
|
||||
draw_text(ctx, font, textstr, Vec2{textx, texty}, text_color)
|
||||
draw_rect(ctx, Rect{textx + headx, texty, 1, texth}, text_color)
|
||||
pop_clip_rect(ctx)
|
||||
} else {
|
||||
draw_control_text(ctx, textstr, r, .TEXT, opt)
|
||||
|
||||
Vendored
+45
-13
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user