Merge remote-tracking branch 'offical/master'

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