Merge remote-tracking branch 'origin/master' into more-windows-comm

This commit is contained in:
jason
2024-06-19 12:33:13 -04:00
62 changed files with 12642 additions and 690 deletions

View File

@@ -397,11 +397,34 @@ Logger :: struct {
options: Logger_Options,
}
Random_Generator_Mode :: enum {
Read,
Reset,
Query_Info,
}
Random_Generator_Query_Info_Flag :: enum u32 {
Cryptographic,
Uniform,
External_Entropy,
Resettable,
}
Random_Generator_Query_Info :: distinct bit_set[Random_Generator_Query_Info_Flag; u32]
Random_Generator_Proc :: #type proc(data: rawptr, mode: Random_Generator_Mode, p: []byte)
Random_Generator :: struct {
procedure: Random_Generator_Proc,
data: rawptr,
}
Context :: struct {
allocator: Allocator,
temp_allocator: Allocator,
assertion_failure_proc: Assertion_Failure_Proc,
logger: Logger,
random_generator: Random_Generator,
user_ptr: rawptr,
user_index: int,
@@ -708,6 +731,9 @@ __init_context :: proc "contextless" (c: ^Context) {
c.logger.procedure = default_logger_proc
c.logger.data = nil
c.random_generator.procedure = default_random_generator_proc
c.random_generator.data = nil
}
default_assertion_failure_proc :: proc(prefix, message: string, loc: Source_Code_Location) -> ! {

View File

@@ -12,7 +12,8 @@ Memory_Block :: struct {
capacity: uint,
}
// NOTE: This is for internal use, prefer `Arena` from `core:mem/virtual` if necessary
// NOTE: This is a growing arena that is only used for the default temp allocator.
// For your own growing arena needs, prefer `Arena` from `core:mem/virtual`.
Arena :: struct {
backing_allocator: Allocator,
curr_block: ^Memory_Block,

View File

@@ -0,0 +1,120 @@
package runtime
import "base:intrinsics"
@(require_results)
random_generator_read_bytes :: proc(rg: Random_Generator, p: []byte) -> bool {
if rg.procedure != nil {
rg.procedure(rg.data, .Read, p)
return true
}
return false
}
@(require_results)
random_generator_read_ptr :: proc(rg: Random_Generator, p: rawptr, len: uint) -> bool {
if rg.procedure != nil {
rg.procedure(rg.data, .Read, ([^]byte)(p)[:len])
return true
}
return false
}
@(require_results)
random_generator_query_info :: proc(rg: Random_Generator) -> (info: Random_Generator_Query_Info) {
if rg.procedure != nil {
rg.procedure(rg.data, .Query_Info, ([^]byte)(&info)[:size_of(info)])
}
return
}
random_generator_reset_bytes :: proc(rg: Random_Generator, p: []byte) {
if rg.procedure != nil {
rg.procedure(rg.data, .Reset, p)
}
}
random_generator_reset_u64 :: proc(rg: Random_Generator, p: u64) {
if rg.procedure != nil {
p := p
rg.procedure(rg.data, .Reset, ([^]byte)(&p)[:size_of(p)])
}
}
Default_Random_State :: struct {
state: u64,
inc: u64,
}
default_random_generator_proc :: proc(data: rawptr, mode: Random_Generator_Mode, p: []byte) {
@(require_results)
read_u64 :: proc "contextless" (r: ^Default_Random_State) -> u64 {
old_state := r.state
r.state = old_state * 6364136223846793005 + (r.inc|1)
xor_shifted := (((old_state >> 59) + 5) ~ old_state) * 12605985483714917081
rot := (old_state >> 59)
return (xor_shifted >> rot) | (xor_shifted << ((-rot) & 63))
}
@(thread_local)
global_rand_seed: Default_Random_State
init :: proc "contextless" (r: ^Default_Random_State, seed: u64) {
seed := seed
if seed == 0 {
seed = u64(intrinsics.read_cycle_counter())
}
r.state = 0
r.inc = (seed << 1) | 1
_ = read_u64(r)
r.state += seed
_ = read_u64(r)
}
r: ^Default_Random_State = ---
if data == nil {
r = &global_rand_seed
} else {
r = cast(^Default_Random_State)data
}
switch mode {
case .Read:
if r.state == 0 && r.inc == 0 {
init(r, 0)
}
pos := i8(0)
val := u64(0)
for &v in p {
if pos == 0 {
val = read_u64(r)
pos = 7
}
v = byte(val)
val >>= 8
pos -= 1
}
case .Reset:
seed: u64
mem_copy_non_overlapping(&seed, raw_data(p), min(size_of(seed), len(p)))
init(r, seed)
case .Query_Info:
if len(p) != size_of(Random_Generator_Query_Info) {
return
}
info := (^Random_Generator_Query_Info)(raw_data(p))
info^ += {.Uniform, .Resettable}
}
}
default_random_generator :: proc "contextless" (state: ^Default_Random_State = nil) -> Random_Generator {
return {
procedure = default_random_generator_proc,
data = state,
}
}

View File

@@ -87,7 +87,7 @@ init_cmp :: proc(
init_ordered :: proc(
t: ^$T/Tree($Value),
node_allocator := context.allocator,
) where intrinsics.type_is_ordered_numeric(Value) {
) where intrinsics.type_is_ordered(Value) {
init_cmp(t, slice.cmp_proc(Value), node_allocator)
}

View File

@@ -79,7 +79,7 @@ init_cmp :: proc(t: ^$T/Tree($Key, $Value), cmp_fn: proc(a, b: Key) -> Ordering,
// 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_ordered :: proc(t: ^$T/Tree($Key, $Value), node_allocator := context.allocator) where intrinsics.type_is_ordered(Key) {
init_cmp(t, slice.cmp_proc(Key), node_allocator)
}

View File

@@ -4,6 +4,7 @@ helper routines.
*/
package crypto
import "base:runtime"
import "core:mem"
// compare_constant_time returns 1 iff a and b are equal, 0 otherwise.
@@ -58,3 +59,24 @@ rand_bytes :: proc (dst: []byte) {
_rand_bytes(dst)
}
random_generator :: proc() -> runtime.Random_Generator {
return {
procedure = proc(data: rawptr, mode: runtime.Random_Generator_Mode, p: []byte) {
switch mode {
case .Read:
rand_bytes(p)
case .Reset:
// do nothing
case .Query_Info:
if len(p) != size_of(runtime.Random_Generator_Query_Info) {
return
}
info := (^runtime.Random_Generator_Query_Info)(raw_data(p))
info^ += {.Uniform, .Cryptographic, .External_Entropy}
}
},
data = nil,
}
}

28
core/flags/LICENSE Normal file
View File

@@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2024, Feoramund
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

38
core/flags/constants.odin Normal file
View File

@@ -0,0 +1,38 @@
package flags
import "core:time"
// Set to true to compile with support for core named types disabled, as a
// fallback in the event your platform does not support one of the types, or
// you have no need for them and want a smaller binary.
NO_CORE_NAMED_TYPES :: #config(ODIN_CORE_FLAGS_NO_CORE_NAMED_TYPES, false)
// Override support for parsing `time` types.
IMPORTING_TIME :: #config(ODIN_CORE_FLAGS_USE_TIME, time.IS_SUPPORTED)
// Override support for parsing `net` types.
// TODO: Update this when the BSDs are supported.
IMPORTING_NET :: #config(ODIN_CORE_FLAGS_USE_NET, ODIN_OS == .Windows || ODIN_OS == .Linux || ODIN_OS == .Darwin)
TAG_ARGS :: "args"
SUBTAG_NAME :: "name"
SUBTAG_POS :: "pos"
SUBTAG_REQUIRED :: "required"
SUBTAG_HIDDEN :: "hidden"
SUBTAG_VARIADIC :: "variadic"
SUBTAG_FILE :: "file"
SUBTAG_PERMS :: "perms"
SUBTAG_INDISTINCT :: "indistinct"
TAG_USAGE :: "usage"
UNDOCUMENTED_FLAG :: "<This flag has not been documented yet.>"
INTERNAL_VARIADIC_FLAG :: "varg"
RESERVED_HELP_FLAG :: "help"
RESERVED_HELP_FLAG_SHORT :: "h"
// If there are more than this number of flags in total, only the required and
// positional flags will be shown in the one-line usage summary.
ONE_LINE_FLAG_CUTOFF_COUNT :: 16

181
core/flags/doc.odin Normal file
View File

@@ -0,0 +1,181 @@
/*
package flags implements a command-line argument parser.
It works by using Odin's run-time type information to determine where and how
to store data on a struct provided by the program. Type conversion is handled
automatically and errors are reported with useful messages.
Command-Line Syntax:
Arguments are treated differently depending on how they're formatted.
The format is similar to the Odin binary's way of handling compiler flags.
```
type handling
------------ ------------------------
<positional> depends on struct layout
-<flag> set a bool true
-<flag:option> set flag to option
-<flag=option> set flag to option, alternative syntax
-<map>:<key>=<value> set map[key] to value
```
Struct Tags:
Users of the `core:encoding/json` package may be familiar with using tags to
annotate struct metadata. The same technique is used here to annotate where
arguments should go and which are required.
Under the `args` tag, there are the following subtags:
- `name=S`: set `S` as the flag's name.
- `pos=N`: place positional argument `N` into this flag.
- `hidden`: hide this flag from the usage documentation.
- `required`: cause verification to fail if this argument is not set.
- `variadic`: take all remaining arguments when set, UNIX-style only.
- `file`: for `os.Handle` types, file open mode.
- `perms`: for `os.Handle` types, file open permissions.
- `indistinct`: allow the setting of distinct types by their base type.
`required` may be given a range specifier in the following formats:
```
min
<max
min<max
```
`max` is not inclusive in this range, as noted by the less-than `<` sign, so if
you want to require 3 and only 3 arguments in a dynamic array, you would
specify `required=3<4`.
`variadic` may be given a number (`variadic=N`) above 1 to limit how many extra
arguments it consumes.
`file` determines the file open mode for an `os.Handle`.
It accepts a string of flags that can be mixed together:
- r: read
- w: write
- c: create, create the file if it doesn't exist
- a: append, add any new writes to the end of the file
- t: truncate, erase the file on open
`perms` determines the file open permissions for an `os.Handle`.
The permissions are represented by three numbers in octal format. The first
number is the owner, the second is the group, and the third is other. Read is
represented by 4, write by 2, and execute by 1.
These numbers are added together to get combined permissions. For example, 644
represents read/write for the owner, read for the group, and read for other.
Note that this may only have effect on UNIX-like platforms. By default, `perms`
is set to 444 when only reading and 644 when writing.
`indistinct` tells the parser that it's okay to treat distinct types as their
underlying base type. Normally, the parser will hand those types off to the
custom type setter (more about that later) if one is available, if it doesn't
know how to handle the type.
Usage Tag:
There is also the `usage` tag, which is a plain string to be printed alongside
the flag in the usage output. If `usage` contains a newline, it will be
properly aligned when printed.
All surrounding whitespace is trimmed when formatting with multiple lines.
Supported Flag Data Types:
- all booleans
- all integers
- all floats
- all enums
- all complex numbers
- all quaternions
- all bit_sets
- `string` and `cstring`
- `rune`
- `os.Handle`
- `time.Time`
- `datetime.DateTime`
- `net.Host_Or_Endpoint`,
- additional custom types, see Custom Types below
- `dynamic` arrays with element types of the above
- `map[string]`s or `map[cstring]`s with value types of the above
Validation:
The parser will ensure `required` arguments are set, if no errors occurred
during parsing. This is on by default.
Additionally, you may call `register_flag_checker` to set your own argument
validation procedure that will be called after the default checker.
Strict:
The parser will return on the first error and stop parsing. This is on by
default. Otherwise, all arguments that can be parsed, will be, and only the
last error is returned.
Error Messages:
All error message strings are allocated using the context's `temp_allocator`,
so if you need them to persist, make sure to clone the underlying `message`.
Help:
By default, `-h` and `-help` are reserved flags which raise their own error
type when set, allowing the program to handle the request differently from
other errors.
Custom Types:
You may specify your own type setter for program-specific structs and other
named types. Call `register_type_setter` with an appropriate proc before
calling any of the parsing procs.
A compliant `Custom_Type_Setter` must return three values:
- an error message if one occurred,
- a boolean indicating if the proc handles the type, and
- an `Allocator_Error` if any occurred.
If the setter does not handle the type, simply return without setting any of
the values.
UNIX-style:
This package also supports parsing arguments in a limited flavor of UNIX.
Odin and UNIX style are mutually exclusive, and which one to be used is chosen
at parse time.
```
--flag
--flag=argument
--flag argument
--flag argument repeating-argument
```
`-flag` may also be substituted for `--flag`.
Do note that map flags are not currently supported in this parsing style.
Example:
A complete example is given in the `example` subdirectory.
*/
package flags

50
core/flags/errors.odin Normal file
View File

@@ -0,0 +1,50 @@
package flags
import "core:os"
Parse_Error_Reason :: enum {
None,
// An extra positional argument was given, and there is no `varg` field.
Extra_Positional,
// The underlying type does not support the string value it is being set to.
Bad_Value,
// No flag was given by the user.
No_Flag,
// No value was given by the user.
No_Value,
// The flag on the struct is missing.
Missing_Flag,
// The type itself isn't supported.
Unsupported_Type,
}
// Raised during parsing, naturally.
Parse_Error :: struct {
reason: Unified_Parse_Error_Reason,
message: string,
}
// Raised during parsing.
// Provides more granular information than what just a string could hold.
Open_File_Error :: struct {
filename: string,
errno: os.Errno,
mode: int,
perms: int,
}
// Raised during parsing.
Help_Request :: distinct bool
// Raised after parsing, during validation.
Validation_Error :: struct {
message: string,
}
Error :: union {
Parse_Error,
Open_File_Error,
Help_Request,
Validation_Error,
}

View File

@@ -0,0 +1,9 @@
//+build freebsd, netbsd, openbsd
package flags
import "base:runtime"
Unified_Parse_Error_Reason :: union #shared_nil {
Parse_Error_Reason,
runtime.Allocator_Error,
}

View File

@@ -0,0 +1,11 @@
//+build !freebsd !netbsd !openbsd
package flags
import "base:runtime"
import "core:net"
Unified_Parse_Error_Reason :: union #shared_nil {
Parse_Error_Reason,
runtime.Allocator_Error,
net.Parse_Endpoint_Error,
}

View File

@@ -0,0 +1,132 @@
package core_flags_example
import "base:runtime"
import "core:flags"
import "core:fmt"
import "core:net"
import "core:os"
import "core:time/datetime"
Fixed_Point1_1 :: struct {
integer: u8,
fractional: u8,
}
Optimization_Level :: enum {
Slow,
Fast,
Warp_Speed,
Ludicrous_Speed,
}
// It's simple but powerful.
my_custom_type_setter :: proc(
data: rawptr,
data_type: typeid,
unparsed_value: string,
args_tag: string,
) -> (
error: string,
handled: bool,
alloc_error: runtime.Allocator_Error,
) {
if data_type == Fixed_Point1_1 {
handled = true
ptr := cast(^Fixed_Point1_1)data
// precision := flags.get_subtag(args_tag, "precision")
if len(unparsed_value) == 3 {
ptr.integer = unparsed_value[0] - '0'
ptr.fractional = unparsed_value[2] - '0'
} else {
error = "Incorrect format. Must be in the form of `i.f`."
}
// Perform sanity checking here in the type parsing phase.
//
// The validation phase is flag-specific.
if !(0 <= ptr.integer && ptr.integer < 10) || !(0 <= ptr.fractional && ptr.fractional < 10) {
error = "Incorrect format. Must be between `0.0` and `9.9`."
}
}
return
}
my_custom_flag_checker :: proc(
model: rawptr,
name: string,
value: any,
args_tag: string,
) -> (error: string) {
if name == "iterations" {
v := value.(int)
if !(1 <= v && v < 5) {
error = "Iterations only supports 1 ..< 5."
}
}
return
}
Distinct_Int :: distinct int
main :: proc() {
Options :: struct {
file: os.Handle `args:"pos=0,required,file=r" usage:"Input file."`,
output: os.Handle `args:"pos=1,file=cw" usage:"Output file."`,
hub: net.Host_Or_Endpoint `usage:"Internet address to contact for updates."`,
schedule: datetime.DateTime `usage:"Launch tasks at this time."`,
opt: Optimization_Level `usage:"Optimization level."`,
todo: [dynamic]string `usage:"Todo items."`,
accuracy: Fixed_Point1_1 `args:"required" usage:"Lenience in FLOP calculations."`,
iterations: int `usage:"Run this many times."`,
// Note how the parser will transform this flag's name into `special-int`.
special_int: Distinct_Int `args:"indistinct" usage:"Able to set distinct types."`,
quat: quaternion256,
bits: bit_set[0..<8],
// Many different requirement styles:
// gadgets: [dynamic]string `args:"required=1" usage:"gadgets"`,
// widgets: [dynamic]string `args:"required=<3" usage:"widgets"`,
// foos: [dynamic]string `args:"required=2<4"`,
// bars: [dynamic]string `args:"required=3<4"`,
// bots: [dynamic]string `args:"required"`,
// (Maps) Only available in Odin style:
// assignments: map[string]u8 `args:"name=assign" usage:"Number of jobs per worker."`,
// (Variadic) Only available in UNIX style:
// bots: [dynamic]string `args:"variadic=2,required"`,
verbose: bool `usage:"Show verbose output."`,
debug: bool `args:"hidden" usage:"print debug info"`,
varg: [dynamic]string `usage:"Any extra arguments go here."`,
}
opt: Options
style : flags.Parsing_Style = .Odin
flags.register_type_setter(my_custom_type_setter)
flags.register_flag_checker(my_custom_flag_checker)
flags.parse_or_exit(&opt, os.args, style)
fmt.printfln("%#v", opt)
if opt.output != 0 {
os.write_string(opt.output, "Hellope!\n")
}
}

View File

@@ -0,0 +1,262 @@
//+private
package flags
import "base:intrinsics"
@require import "base:runtime"
import "core:container/bit_array"
@require import "core:fmt"
@require import "core:mem"
import "core:reflect"
@require import "core:strconv"
@require import "core:strings"
// Push a positional argument onto a data struct, checking for specified
// positionals first before adding it to a fallback field.
@(optimization_mode="size")
push_positional :: #force_no_inline proc (model: ^$T, parser: ^Parser, arg: string) -> (error: Error) {
if bit_array.get(&parser.filled_pos, parser.filled_pos.max_index) {
// The max index is set, which means we're out of space.
// Add one free bit by setting the index above to false.
bit_array.set(&parser.filled_pos, 1 + parser.filled_pos.max_index, false)
}
pos: int = ---
{
iter := bit_array.make_iterator(&parser.filled_pos)
ok: bool
pos, ok = bit_array.iterate_by_unset(&iter)
// This may be an allocator error.
assert(ok, "Unable to find a free spot in the positional bit_array.")
}
field, index, has_pos_assigned := get_field_by_pos(model, pos)
if !has_pos_assigned {
when intrinsics.type_has_field(T, INTERNAL_VARIADIC_FLAG) {
// Add it to the fallback array.
field = reflect.struct_field_by_name(T, INTERNAL_VARIADIC_FLAG)
} else {
return Parse_Error {
.Extra_Positional,
fmt.tprintf("Got extra positional argument `%s` with nowhere to store it.", arg),
}
}
}
ptr := cast(rawptr)(cast(uintptr)model + field.offset)
args_tag, _ := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
field_name := get_field_name(field)
error = parse_and_set_pointer_by_type(ptr, arg, field.type, args_tag)
#partial switch &specific_error in error {
case Parse_Error:
specific_error.message = fmt.tprintf("Unable to set positional #%i (%s) of type %v to `%s`.%s%s",
pos,
field_name,
field.type,
arg,
" " if len(specific_error.message) > 0 else "",
specific_error.message)
case nil:
bit_array.set(&parser.filled_pos, pos)
bit_array.set(&parser.fields_set, index)
}
return
}
register_field :: proc(parser: ^Parser, field: reflect.Struct_Field, index: int) {
if pos, ok := get_field_pos(field); ok {
bit_array.set(&parser.filled_pos, pos)
}
bit_array.set(&parser.fields_set, index)
}
// Set a `-flag` argument, Odin-style.
@(optimization_mode="size")
set_odin_flag :: proc(model: ^$T, parser: ^Parser, name: string) -> (error: Error) {
// We make a special case for help requests.
switch name {
case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
return Help_Request{}
}
field, index := get_field_by_name(model, name) or_return
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Boolean:
ptr := cast(^bool)(cast(uintptr)model + field.offset)
ptr^ = true
case:
return Parse_Error {
.Bad_Value,
fmt.tprintf("Unable to set `%s` of type %v to true.", name, field.type),
}
}
register_field(parser, field, index)
return
}
// Set a `-flag` argument, UNIX-style.
@(optimization_mode="size")
set_unix_flag :: proc(model: ^$T, parser: ^Parser, name: string) -> (future_args: int, error: Error) {
// We make a special case for help requests.
switch name {
case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
return 0, Help_Request{}
}
field, index := get_field_by_name(model, name) or_return
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Boolean:
ptr := cast(^bool)(cast(uintptr)model + field.offset)
ptr^ = true
case runtime.Type_Info_Dynamic_Array:
future_args = 1
if tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
if length, is_variadic := get_struct_subtag(tag, SUBTAG_VARIADIC); is_variadic {
// Variadic arrays may specify how many arguments they consume at once.
// Otherwise, they take everything that's left.
if value, value_ok := strconv.parse_u64_of_base(length, 10); value_ok {
future_args = cast(int)value
} else {
future_args = max(int)
}
}
}
case:
// `--flag`, waiting on its value.
future_args = 1
}
register_field(parser, field, index)
return
}
// Set a `-flag:option` argument.
@(optimization_mode="size")
set_option :: proc(model: ^$T, parser: ^Parser, name, option: string) -> (error: Error) {
field, index := get_field_by_name(model, name) or_return
if len(option) == 0 {
return Parse_Error {
.No_Value,
fmt.tprintf("Setting `%s` to an empty value is meaningless.", name),
}
}
// Guard against incorrect syntax.
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Map:
return Parse_Error {
.No_Value,
fmt.tprintf("Unable to set `%s` of type %v to `%s`. Are you missing an `=`? The correct format is `map:key=value`.", name, field.type, option),
}
}
ptr := cast(rawptr)(cast(uintptr)model + field.offset)
args_tag := reflect.struct_tag_get(field.tag, TAG_ARGS)
error = parse_and_set_pointer_by_type(ptr, option, field.type, args_tag)
#partial switch &specific_error in error {
case Parse_Error:
specific_error.message = fmt.tprintf("Unable to set `%s` of type %v to `%s`.%s%s",
name,
field.type,
option,
" " if len(specific_error.message) > 0 else "",
specific_error.message)
case nil:
register_field(parser, field, index)
}
return
}
// Set a `-map:key=value` argument.
@(optimization_mode="size")
set_key_value :: proc(model: ^$T, parser: ^Parser, name, key, value: string) -> (error: Error) {
field, index := get_field_by_name(model, name) or_return
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Map:
key := key
key_ptr := cast(rawptr)&key
key_cstr: cstring
if reflect.is_cstring(specific_type_info.key) {
// We clone the key here, because it's liable to be a slice of an
// Odin string, and we need to put a NUL terminator in it.
key_cstr = strings.clone_to_cstring(key)
key_ptr = &key_cstr
}
defer if key_cstr != nil {
delete(key_cstr)
}
raw_map := (^runtime.Raw_Map)(cast(uintptr)model + field.offset)
hash := specific_type_info.map_info.key_hasher(key_ptr, runtime.map_seed(raw_map^))
backing_alloc := false
elem_backing: []byte
value_ptr: rawptr
if raw_map.allocator.procedure == nil {
raw_map.allocator = context.allocator
} else {
value_ptr = runtime.__dynamic_map_get(raw_map,
specific_type_info.map_info,
hash,
key_ptr,
)
}
if value_ptr == nil {
alloc_error: runtime.Allocator_Error = ---
elem_backing, alloc_error = mem.alloc_bytes(specific_type_info.value.size, specific_type_info.value.align)
if elem_backing == nil {
return Parse_Error {
alloc_error,
"Failed to allocate element backing for map value.",
}
}
backing_alloc = true
value_ptr = raw_data(elem_backing)
}
args_tag, _ := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
error = parse_and_set_pointer_by_type(value_ptr, value, specific_type_info.value, args_tag)
#partial switch &specific_error in error {
case Parse_Error:
specific_error.message = fmt.tprintf("Unable to set `%s` of type %v with key=value: `%s`=`%s`.%s%s",
name,
field.type,
key,
value,
" " if len(specific_error.message) > 0 else "",
specific_error.message)
}
if backing_alloc {
runtime.__dynamic_map_set(raw_map,
specific_type_info.map_info,
hash,
key_ptr,
value_ptr,
)
delete(elem_backing)
}
register_field(parser, field, index)
return
}
return Parse_Error {
.Bad_Value,
fmt.tprintf("Unable to set `%s` of type %v with key=value: `%s`=`%s`.", name, field.type, key, value),
}
}

View File

@@ -0,0 +1,170 @@
//+private
package flags
import "core:container/bit_array"
import "core:strconv"
import "core:strings"
// Used to group state together.
Parser :: struct {
// `fields_set` tracks which arguments have been set.
// It uses their struct field index.
fields_set: bit_array.Bit_Array,
// `filled_pos` tracks which arguments have been filled into positional
// spots, much like how `fmt` treats them.
filled_pos: bit_array.Bit_Array,
}
parse_one_odin_arg :: proc(model: ^$T, parser: ^Parser, arg: string) -> (error: Error) {
arg := arg
if strings.has_prefix(arg, "-") {
arg = arg[1:]
flag: string
assignment_rune: rune
find_assignment: for r, i in arg {
switch r {
case ':', '=':
assignment_rune = r
flag = arg[:i]
arg = arg[1 + i:]
break find_assignment
case:
continue find_assignment
}
}
if assignment_rune == 0 {
if len(arg) == 0 {
return Parse_Error {
.No_Flag,
"No flag was given.",
}
}
// -flag
set_odin_flag(model, parser, arg) or_return
} else if assignment_rune == ':' {
// -flag:option <OR> -map:key=value
error = set_option(model, parser, flag, arg)
if error != nil {
// -flag:option did not work, so this may be a -map:key=value set.
find_equals: for r, i in arg {
if r == '=' {
key := arg[:i]
arg = arg[1 + i:]
error = set_key_value(model, parser, flag, key, arg)
break find_equals
}
}
}
} else {
// -flag=option, alternative syntax
set_option(model, parser, flag, arg) or_return
}
} else {
// positional
error = push_positional(model, parser, arg)
}
return
}
parse_one_unix_arg :: proc(model: ^$T, parser: ^Parser, arg: string) -> (
future_args: int,
current_flag: string,
error: Error,
) {
arg := arg
if strings.has_prefix(arg, "-") {
// -flag
arg = arg[1:]
if strings.has_prefix(arg, "-") {
// Allow `--` to function as `-`.
arg = arg[1:]
if len(arg) == 0 {
// `--`, and only `--`.
// Everything from now on will be treated as an argument.
future_args = max(int)
current_flag = INTERNAL_VARIADIC_FLAG
return
}
}
flag: string
find_assignment: for r, i in arg {
if r == '=' {
// --flag=option
flag = arg[:i]
arg = arg[1 + i:]
error = set_option(model, parser, flag, arg)
return
}
}
// --flag option, potentially
future_args = set_unix_flag(model, parser, arg) or_return
current_flag = arg
} else {
// positional
error = push_positional(model, parser, arg)
}
return
}
// Parse a number of requirements specifier.
//
// Examples:
//
// `min`
// `<max`
// `min<max`
parse_requirements :: proc(str: string) -> (minimum, maximum: int, ok: bool) {
if len(str) == 0 {
return 1, max(int), true
}
if less_than := strings.index_byte(str, '<'); less_than != -1 {
if len(str) == 1 {
return 0, 0, false
}
#no_bounds_check left := str[:less_than]
#no_bounds_check right := str[1 + less_than:]
if left_value, parse_ok := strconv.parse_u64_of_base(left, 10); parse_ok {
minimum = cast(int)left_value
} else if len(left) > 0 {
return 0, 0, false
}
if right_value, parse_ok := strconv.parse_u64_of_base(right, 10); parse_ok {
maximum = cast(int)right_value
} else if len(right) > 0 {
return 0, 0, false
} else {
maximum = max(int)
}
} else {
if value, parse_ok := strconv.parse_u64_of_base(str, 10); parse_ok {
minimum = cast(int)value
maximum = max(int)
} else {
return 0, 0, false
}
}
ok = true
return
}

View File

@@ -0,0 +1,536 @@
//+private
package flags
import "base:intrinsics"
import "base:runtime"
import "core:fmt"
import "core:mem"
import "core:os"
import "core:reflect"
import "core:strconv"
import "core:strings"
@require import "core:time"
@require import "core:time/datetime"
import "core:unicode/utf8"
@(optimization_mode="size")
parse_and_set_pointer_by_base_type :: proc(ptr: rawptr, str: string, type_info: ^runtime.Type_Info) -> bool {
bounded_int :: proc(value, min, max: i128) -> (result: i128, ok: bool) {
return value, min <= value && value <= max
}
bounded_uint :: proc(value, max: u128) -> (result: u128, ok: bool) {
return value, value <= max
}
// NOTE(Feoramund): This procedure has been written with the goal in mind
// of generating the least amount of assembly, given that this library is
// likely to be called once and forgotten.
//
// I've rewritten the switch tables below in 3 different ways, and the
// current one generates the least amount of code for me on Linux AMD64.
//
// The other two ways were:
//
// - the original implementation: use of parametric polymorphism which led
// to dozens of functions generated, one for each type.
//
// - a `value, ok` assignment statement with the `or_return` done at the
// end of the switch, instead of inline.
//
// This seems to be the smallest way for now.
#partial switch specific_type_info in type_info.variant {
case runtime.Type_Info_Integer:
if specific_type_info.signed {
value := strconv.parse_i128(str) or_return
switch type_info.id {
case i8: (cast(^i8) ptr)^ = cast(i8) bounded_int(value, cast(i128)min(i8), cast(i128)max(i8) ) or_return
case i16: (cast(^i16) ptr)^ = cast(i16) bounded_int(value, cast(i128)min(i16), cast(i128)max(i16) ) or_return
case i32: (cast(^i32) ptr)^ = cast(i32) bounded_int(value, cast(i128)min(i32), cast(i128)max(i32) ) or_return
case i64: (cast(^i64) ptr)^ = cast(i64) bounded_int(value, cast(i128)min(i64), cast(i128)max(i64) ) or_return
case i128: (cast(^i128) ptr)^ = value
case int: (cast(^int) ptr)^ = cast(int) bounded_int(value, cast(i128)min(int), cast(i128)max(int) ) or_return
case i16le: (cast(^i16le) ptr)^ = cast(i16le) bounded_int(value, cast(i128)min(i16le), cast(i128)max(i16le) ) or_return
case i32le: (cast(^i32le) ptr)^ = cast(i32le) bounded_int(value, cast(i128)min(i32le), cast(i128)max(i32le) ) or_return
case i64le: (cast(^i64le) ptr)^ = cast(i64le) bounded_int(value, cast(i128)min(i64le), cast(i128)max(i64le) ) or_return
case i128le: (cast(^i128le)ptr)^ = cast(i128le) bounded_int(value, cast(i128)min(i128le), cast(i128)max(i128le)) or_return
case i16be: (cast(^i16be) ptr)^ = cast(i16be) bounded_int(value, cast(i128)min(i16be), cast(i128)max(i16be) ) or_return
case i32be: (cast(^i32be) ptr)^ = cast(i32be) bounded_int(value, cast(i128)min(i32be), cast(i128)max(i32be) ) or_return
case i64be: (cast(^i64be) ptr)^ = cast(i64be) bounded_int(value, cast(i128)min(i64be), cast(i128)max(i64be) ) or_return
case i128be: (cast(^i128be)ptr)^ = cast(i128be) bounded_int(value, cast(i128)min(i128be), cast(i128)max(i128be)) or_return
}
} else {
value := strconv.parse_u128(str) or_return
switch type_info.id {
case u8: (cast(^u8) ptr)^ = cast(u8) bounded_uint(value, cast(u128)max(u8) ) or_return
case u16: (cast(^u16) ptr)^ = cast(u16) bounded_uint(value, cast(u128)max(u16) ) or_return
case u32: (cast(^u32) ptr)^ = cast(u32) bounded_uint(value, cast(u128)max(u32) ) or_return
case u64: (cast(^u64) ptr)^ = cast(u64) bounded_uint(value, cast(u128)max(u64) ) or_return
case u128: (cast(^u128) ptr)^ = value
case uint: (cast(^uint) ptr)^ = cast(uint) bounded_uint(value, cast(u128)max(uint) ) or_return
case uintptr: (cast(^uintptr)ptr)^ = cast(uintptr) bounded_uint(value, cast(u128)max(uintptr)) or_return
case u16le: (cast(^u16le) ptr)^ = cast(u16le) bounded_uint(value, cast(u128)max(u16le) ) or_return
case u32le: (cast(^u32le) ptr)^ = cast(u32le) bounded_uint(value, cast(u128)max(u32le) ) or_return
case u64le: (cast(^u64le) ptr)^ = cast(u64le) bounded_uint(value, cast(u128)max(u64le) ) or_return
case u128le: (cast(^u128le) ptr)^ = cast(u128le) bounded_uint(value, cast(u128)max(u128le) ) or_return
case u16be: (cast(^u16be) ptr)^ = cast(u16be) bounded_uint(value, cast(u128)max(u16be) ) or_return
case u32be: (cast(^u32be) ptr)^ = cast(u32be) bounded_uint(value, cast(u128)max(u32be) ) or_return
case u64be: (cast(^u64be) ptr)^ = cast(u64be) bounded_uint(value, cast(u128)max(u64be) ) or_return
case u128be: (cast(^u128be) ptr)^ = cast(u128be) bounded_uint(value, cast(u128)max(u128be) ) or_return
}
}
case runtime.Type_Info_Rune:
if utf8.rune_count_in_string(str) != 1 {
return false
}
(cast(^rune)ptr)^ = utf8.rune_at_pos(str, 0)
case runtime.Type_Info_Float:
value := strconv.parse_f64(str) or_return
switch type_info.id {
case f16: (cast(^f16) ptr)^ = cast(f16) value
case f32: (cast(^f32) ptr)^ = cast(f32) value
case f64: (cast(^f64) ptr)^ = value
case f16le: (cast(^f16le)ptr)^ = cast(f16le) value
case f32le: (cast(^f32le)ptr)^ = cast(f32le) value
case f64le: (cast(^f64le)ptr)^ = cast(f64le) value
case f16be: (cast(^f16be)ptr)^ = cast(f16be) value
case f32be: (cast(^f32be)ptr)^ = cast(f32be) value
case f64be: (cast(^f64be)ptr)^ = cast(f64be) value
}
case runtime.Type_Info_Complex:
value := strconv.parse_complex128(str) or_return
switch type_info.id {
case complex128: (cast(^complex128)ptr)^ = value
case complex64: (cast(^complex64) ptr)^ = cast(complex64)value
case complex32: (cast(^complex32) ptr)^ = cast(complex32)value
}
case runtime.Type_Info_Quaternion:
value := strconv.parse_quaternion256(str) or_return
switch type_info.id {
case quaternion256: (cast(^quaternion256)ptr)^ = value
case quaternion128: (cast(^quaternion128)ptr)^ = cast(quaternion128)value
case quaternion64: (cast(^quaternion64) ptr)^ = cast(quaternion64)value
}
case runtime.Type_Info_String:
if specific_type_info.is_cstring {
cstr_ptr := cast(^cstring)ptr
if cstr_ptr != nil {
// Prevent memory leaks from us setting this value multiple times.
delete(cstr_ptr^)
}
cstr_ptr^ = strings.clone_to_cstring(str)
} else {
(cast(^string)ptr)^ = str
}
case runtime.Type_Info_Boolean:
value := strconv.parse_bool(str) or_return
switch type_info.id {
case bool: (cast(^bool) ptr)^ = value
case b8: (cast(^b8) ptr)^ = cast(b8) value
case b16: (cast(^b16) ptr)^ = cast(b16) value
case b32: (cast(^b32) ptr)^ = cast(b32) value
case b64: (cast(^b64) ptr)^ = cast(b64) value
}
case runtime.Type_Info_Bit_Set:
// Parse a string of 1's and 0's, from left to right,
// least significant bit to most significant bit.
value: u128
// NOTE: `upper` is inclusive, i.e: `0..=31`
max_bit_index := cast(u128)(1 + specific_type_info.upper - specific_type_info.lower)
bit_index : u128 = 0
#no_bounds_check for string_index : uint = 0; string_index < len(str); string_index += 1 {
if bit_index == max_bit_index {
// The string's too long for this bit_set.
return false
}
switch str[string_index] {
case '1':
value |= 1 << bit_index
bit_index += 1
case '0':
bit_index += 1
continue
case '_':
continue
case:
return false
}
}
if specific_type_info.underlying != nil {
set_unbounded_integer_by_type(ptr, value, specific_type_info.underlying.id)
} else {
switch 8*type_info.size {
case 8: (cast(^u8) ptr)^ = cast(u8) value
case 16: (cast(^u16) ptr)^ = cast(u16) value
case 32: (cast(^u32) ptr)^ = cast(u32) value
case 64: (cast(^u64) ptr)^ = cast(u64) value
case 128: (cast(^u128) ptr)^ = cast(u128) value
}
}
case:
fmt.panicf("Unsupported base data type: %v", specific_type_info)
}
return true
}
// This proc exists to make error handling easier, since everything in the base
// type one above works on booleans. It's a simple parsing error if it's false.
//
// However, here we have to be more careful about how we handle errors,
// especially with files.
//
// We want to provide as informative as an error as we can.
@(optimization_mode="size", disabled=NO_CORE_NAMED_TYPES)
parse_and_set_pointer_by_named_type :: proc(ptr: rawptr, str: string, data_type: typeid, arg_tag: string, out_error: ^Error) {
// Core types currently supported:
//
// - os.Handle
// - time.Time
// - datetime.DateTime
// - net.Host_Or_Endpoint
GENERIC_RFC_3339_ERROR :: "Invalid RFC 3339 string. Try this format: `yyyy-mm-ddThh:mm:ssZ`, for example `2024-02-29T16:30:00Z`."
out_error^ = nil
if data_type == os.Handle {
// NOTE: `os` is hopefully available everywhere, even if it might panic on some calls.
wants_read := false
wants_write := false
mode: int
if file, ok := get_struct_subtag(arg_tag, SUBTAG_FILE); ok {
for i := 0; i < len(file); i += 1 {
#no_bounds_check switch file[i] {
case 'r': wants_read = true
case 'w': wants_write = true
case 'c': mode |= os.O_CREATE
case 'a': mode |= os.O_APPEND
case 't': mode |= os.O_TRUNC
}
}
}
// Sane default.
// owner/group/other: r--r--r--
perms: int = 0o444
if wants_read && wants_write {
mode |= os.O_RDWR
perms |= 0o200
} else if wants_write {
mode |= os.O_WRONLY
perms |= 0o200
} else {
mode |= os.O_RDONLY
}
if permstr, ok := get_struct_subtag(arg_tag, SUBTAG_PERMS); ok {
if value, parse_ok := strconv.parse_u64_of_base(permstr, 8); parse_ok {
perms = cast(int)value
}
}
handle, errno := os.open(str, mode, perms)
if errno != 0 {
// NOTE(Feoramund): os.Errno is system-dependent, and there's
// currently no good way to translate them all into strings.
//
// The upcoming `os2` package will hopefully solve this.
//
// We can at least provide the number for now, so the user can look
// it up.
out_error^ = Open_File_Error {
str,
errno,
mode,
perms,
}
return
}
(cast(^os.Handle)ptr)^ = handle
return
}
when IMPORTING_TIME {
if data_type == time.Time {
// NOTE: The leap second data is discarded.
res, consumed := time.rfc3339_to_time_utc(str)
if consumed == 0 {
// The RFC 3339 parsing facilities provide no indication as to what
// went wrong, so just treat it as a regular parsing error.
out_error^ = Parse_Error {
.Bad_Value,
GENERIC_RFC_3339_ERROR,
}
return
}
(cast(^time.Time)ptr)^ = res
return
} else if data_type == datetime.DateTime {
// NOTE: The UTC offset and leap second data are discarded.
res, _, _, consumed := time.rfc3339_to_components(str)
if consumed == 0 {
out_error^ = Parse_Error {
.Bad_Value,
GENERIC_RFC_3339_ERROR,
}
return
}
(cast(^datetime.DateTime)ptr)^ = res
return
}
}
when IMPORTING_NET {
if try_net_parse_workaround(data_type, str, ptr, out_error) {
return
}
}
out_error ^= Parse_Error {
// The caller will add more details.
.Unsupported_Type,
"",
}
}
@(optimization_mode="size")
set_unbounded_integer_by_type :: proc(ptr: rawptr, value: $T, data_type: typeid) where intrinsics.type_is_integer(T) {
switch data_type {
case i8: (cast(^i8) ptr)^ = cast(i8) value
case i16: (cast(^i16) ptr)^ = cast(i16) value
case i32: (cast(^i32) ptr)^ = cast(i32) value
case i64: (cast(^i64) ptr)^ = cast(i64) value
case i128: (cast(^i128) ptr)^ = cast(i128) value
case int: (cast(^int) ptr)^ = cast(int) value
case i16le: (cast(^i16le) ptr)^ = cast(i16le) value
case i32le: (cast(^i32le) ptr)^ = cast(i32le) value
case i64le: (cast(^i64le) ptr)^ = cast(i64le) value
case i128le: (cast(^i128le) ptr)^ = cast(i128le) value
case i16be: (cast(^i16be) ptr)^ = cast(i16be) value
case i32be: (cast(^i32be) ptr)^ = cast(i32be) value
case i64be: (cast(^i64be) ptr)^ = cast(i64be) value
case i128be: (cast(^i128be) ptr)^ = cast(i128be) value
case u8: (cast(^u8) ptr)^ = cast(u8) value
case u16: (cast(^u16) ptr)^ = cast(u16) value
case u32: (cast(^u32) ptr)^ = cast(u32) value
case u64: (cast(^u64) ptr)^ = cast(u64) value
case u128: (cast(^u128) ptr)^ = cast(u128) value
case uint: (cast(^uint) ptr)^ = cast(uint) value
case uintptr: (cast(^uintptr)ptr)^ = cast(uintptr) value
case u16le: (cast(^u16le) ptr)^ = cast(u16le) value
case u32le: (cast(^u32le) ptr)^ = cast(u32le) value
case u64le: (cast(^u64le) ptr)^ = cast(u64le) value
case u128le: (cast(^u128le) ptr)^ = cast(u128le) value
case u16be: (cast(^u16be) ptr)^ = cast(u16be) value
case u32be: (cast(^u32be) ptr)^ = cast(u32be) value
case u64be: (cast(^u64be) ptr)^ = cast(u64be) value
case u128be: (cast(^u128be) ptr)^ = cast(u128be) value
case rune: (cast(^rune) ptr)^ = cast(rune) value
case:
fmt.panicf("Unsupported integer backing type: %v", data_type)
}
}
@(optimization_mode="size")
parse_and_set_pointer_by_type :: proc(ptr: rawptr, str: string, type_info: ^runtime.Type_Info, arg_tag: string) -> (error: Error) {
#partial switch specific_type_info in type_info.variant {
case runtime.Type_Info_Named:
if global_custom_type_setter != nil {
// The program gets to go first.
error_message, handled, alloc_error := global_custom_type_setter(ptr, type_info.id, str, arg_tag)
if alloc_error != nil {
// There was an allocation error. Bail out.
return Parse_Error {
alloc_error,
"Custom type setter encountered allocation error.",
}
}
if handled {
// The program handled the type.
if len(error_message) != 0 {
// However, there was an error. Pass it along.
error = Parse_Error {
.Bad_Value,
error_message,
}
}
return
}
}
// Might be a named enum. Need to check here first, since we handle all enums.
if enum_type_info, is_enum := specific_type_info.base.variant.(runtime.Type_Info_Enum); is_enum {
if value, ok := reflect.enum_from_name_any(type_info.id, str); ok {
set_unbounded_integer_by_type(ptr, value, enum_type_info.base.id)
} else {
return Parse_Error {
.Bad_Value,
fmt.tprintf("Invalid value name. Valid names are: %s", enum_type_info.names),
}
}
} else {
parse_and_set_pointer_by_named_type(ptr, str, type_info.id, arg_tag, &error)
if error != nil {
// So far, it's none of the types that we recognize.
// Check to see if we can set it by base type, if allowed.
if _, is_indistinct := get_struct_subtag(arg_tag, SUBTAG_INDISTINCT); is_indistinct {
return parse_and_set_pointer_by_type(ptr, str, specific_type_info.base, arg_tag)
}
}
}
case runtime.Type_Info_Dynamic_Array:
ptr := cast(^runtime.Raw_Dynamic_Array)ptr
// Try to convert the value first.
elem_backing, alloc_error := mem.alloc_bytes(specific_type_info.elem.size, specific_type_info.elem.align)
if alloc_error != nil {
return Parse_Error {
alloc_error,
"Failed to allocate element backing for dynamic array.",
}
}
defer delete(elem_backing)
parse_and_set_pointer_by_type(raw_data(elem_backing), str, specific_type_info.elem, arg_tag) or_return
if !runtime.__dynamic_array_resize(ptr, specific_type_info.elem.size, specific_type_info.elem.align, ptr.len + 1) {
// NOTE: This is purely an assumption that it's OOM.
// Regardless, the resize failed.
return Parse_Error {
runtime.Allocator_Error.Out_Of_Memory,
"Failed to resize dynamic array.",
}
}
subptr := cast(rawptr)(
cast(uintptr)ptr.data +
cast(uintptr)((ptr.len - 1) * specific_type_info.elem.size))
mem.copy(subptr, raw_data(elem_backing), len(elem_backing))
case runtime.Type_Info_Enum:
// This is a nameless enum.
// The code here is virtually the same as above for named enums.
if value, ok := reflect.enum_from_name_any(type_info.id, str); ok {
set_unbounded_integer_by_type(ptr, value, specific_type_info.base.id)
} else {
return Parse_Error {
.Bad_Value,
fmt.tprintf("Invalid value name. Valid names are: %s", specific_type_info.names),
}
}
case:
if !parse_and_set_pointer_by_base_type(ptr, str, type_info) {
return Parse_Error {
// The caller will add more details.
.Bad_Value,
"",
}
}
}
return
}
get_struct_subtag :: get_subtag
get_field_name :: proc(field: reflect.Struct_Field) -> string {
if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
if name_subtag, name_ok := get_struct_subtag(args_tag, SUBTAG_NAME); name_ok {
return name_subtag
}
}
name, _ := strings.replace_all(field.name, "_", "-", context.temp_allocator)
return name
}
get_field_pos :: proc(field: reflect.Struct_Field) -> (int, bool) {
if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
if pos_subtag, pos_ok := get_struct_subtag(args_tag, SUBTAG_POS); pos_ok {
if value, parse_ok := strconv.parse_u64_of_base(pos_subtag, 10); parse_ok {
return cast(int)value, true
}
}
}
return 0, false
}
// Get a struct field by its field name or `name` subtag.
get_field_by_name :: proc(model: ^$T, name: string) -> (result: reflect.Struct_Field, index: int, error: Error) {
for field, i in reflect.struct_fields_zipped(T) {
if get_field_name(field) == name {
return field, i, nil
}
}
error = Parse_Error {
.Missing_Flag,
fmt.tprintf("Unable to find any flag named `%s`.", name),
}
return
}
// Get a struct field by its `pos` subtag.
get_field_by_pos :: proc(model: ^$T, pos: int) -> (result: reflect.Struct_Field, index: int, ok: bool) {
for field, i in reflect.struct_fields_zipped(T) {
args_tag, tag_ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
if !tag_ok {
continue
}
pos_subtag, pos_ok := get_struct_subtag(args_tag, SUBTAG_POS)
if !pos_ok {
continue
}
value, parse_ok := strconv.parse_u64_of_base(pos_subtag, 10)
if parse_ok && cast(int)value == pos {
return field, i, true
}
}
return
}

View File

@@ -0,0 +1,31 @@
//+private
//+build !freebsd !netbsd !openbsd
package flags
import "core:net"
// This proc exists purely as a workaround for import restrictions.
// Returns true if caller should return early.
try_net_parse_workaround :: #force_inline proc (
data_type: typeid,
str: string,
ptr: rawptr,
out_error: ^Error,
) -> bool {
if data_type == net.Host_Or_Endpoint {
addr, net_error := net.parse_hostname_or_endpoint(str)
if net_error != nil {
// We pass along `net.Error` here.
out_error^ = Parse_Error {
net_error,
"Invalid Host/Endpoint.",
}
return true
}
(cast(^net.Host_Or_Endpoint)ptr)^ = addr
return true
}
return false
}

View File

@@ -0,0 +1,244 @@
//+private
package flags
@require import "base:runtime"
@require import "core:container/bit_array"
@require import "core:fmt"
@require import "core:mem"
@require import "core:os"
@require import "core:reflect"
@require import "core:strconv"
@require import "core:strings"
// This proc is used to assert that `T` meets the expectations of the library.
@(optimization_mode="size", disabled=ODIN_DISABLE_ASSERT)
validate_structure :: proc(model_type: $T, style: Parsing_Style, loc := #caller_location) {
positionals_assigned_so_far: bit_array.Bit_Array
defer bit_array.destroy(&positionals_assigned_so_far)
check_fields: for field in reflect.struct_fields_zipped(T) {
if style == .Unix {
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Map:
fmt.panicf("%T.%s is a map type, and these are not supported in UNIX-style parsing mode.",
model_type, field.name, loc = loc)
}
}
name_is_safe := true
defer {
fmt.assertf(name_is_safe, "%T.%s is using a reserved name.",
model_type, field.name, loc = loc)
}
switch field.name {
case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
name_is_safe = false
}
args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
if !ok {
// If it has no args tag, then we've checked all we need to.
// Most of this proc is validating that the subtags are sane.
continue
}
if name, has_name := get_struct_subtag(args_tag, SUBTAG_NAME); has_name {
fmt.assertf(len(name) > 0, "%T.%s has a zero-length `%s`.",
model_type, field.name, SUBTAG_NAME, loc = loc)
fmt.assertf(strings.index(name, " ") == -1, "%T.%s has a `%s` with spaces in it.",
model_type, field.name, SUBTAG_NAME, loc = loc)
switch name {
case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
name_is_safe = false
continue check_fields
case:
name_is_safe = true
}
}
if pos_str, has_pos := get_struct_subtag(args_tag, SUBTAG_POS); has_pos {
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Map:
fmt.panicf("%T.%s has `%s` defined, and this does not make sense on a map type.",
model_type, field.name, SUBTAG_POS, loc = loc)
}
pos_value, pos_ok := strconv.parse_u64_of_base(pos_str, 10)
fmt.assertf(pos_ok, "%T.%s has `%s` defined as %q but cannot be parsed a base-10 integer >= 0.",
model_type, field.name, SUBTAG_POS, pos_str, loc = loc)
fmt.assertf(!bit_array.get(&positionals_assigned_so_far, pos_value), "%T.%s has `%s` set to #%i, but that position has already been assigned to another flag.",
model_type, field.name, SUBTAG_POS, pos_value, loc = loc)
bit_array.set(&positionals_assigned_so_far, pos_value)
}
required_min, required_max: int
if requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
fmt.assertf(!reflect.is_boolean(field.type), "%T.%s is a required boolean. This is disallowed.",
model_type, field.name, loc = loc)
fmt.assertf(field.name != INTERNAL_VARIADIC_FLAG, "%T.%s is defined as required. This is disallowed.",
model_type, field.name, loc = loc)
if len(requirement) > 0 {
if required_min, required_max, ok = parse_requirements(requirement); ok {
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Dynamic_Array:
fmt.assertf(required_min != required_max, "%T.%s has `%s` defined as %q, but the minimum and maximum are the same. Increase the maximum by 1 for an exact number of arguments: (%i<%i)",
model_type,
field.name,
SUBTAG_REQUIRED,
requirement,
required_min,
1 + required_max,
loc = loc)
fmt.assertf(required_min < required_max, "%T.%s has `%s` defined as %q, but the minimum and maximum are swapped.",
model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
case:
fmt.panicf("%T.%s has `%s` defined as %q, but ranges are only supported on dynamic arrays.",
model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
}
} else {
fmt.panicf("%T.%s has `%s` defined as %q, but it cannot be parsed as a valid range.",
model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
}
}
}
if length, is_variadic := get_struct_subtag(args_tag, SUBTAG_VARIADIC); is_variadic {
if value, parse_ok := strconv.parse_u64_of_base(length, 10); parse_ok {
fmt.assertf(value > 0,
"%T.%s has `%s` set to %i. It must be greater than zero.",
model_type, field.name, value, SUBTAG_VARIADIC, loc = loc)
fmt.assertf(value != 1,
"%T.%s has `%s` set to 1. This has no effect.",
model_type, field.name, SUBTAG_VARIADIC, loc = loc)
}
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Dynamic_Array:
fmt.assertf(style != .Odin,
"%T.%s has `%s` defined, but this only makes sense in UNIX-style parsing mode.",
model_type, field.name, SUBTAG_VARIADIC, loc = loc)
case:
fmt.panicf("%T.%s has `%s` defined, but this only makes sense on dynamic arrays.",
model_type, field.name, SUBTAG_VARIADIC, loc = loc)
}
}
allowed_to_define_file_perms: bool = ---
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Map:
allowed_to_define_file_perms = specific_type_info.value.id == os.Handle
case runtime.Type_Info_Dynamic_Array:
allowed_to_define_file_perms = specific_type_info.elem.id == os.Handle
case:
allowed_to_define_file_perms = field.type.id == os.Handle
}
if _, has_file := get_struct_subtag(args_tag, SUBTAG_FILE); has_file {
fmt.assertf(allowed_to_define_file_perms, "%T.%s has `%s` defined, but it is not nor does it contain an `os.Handle` type.",
model_type, field.name, SUBTAG_FILE, loc = loc)
}
if _, has_perms := get_struct_subtag(args_tag, SUBTAG_PERMS); has_perms {
fmt.assertf(allowed_to_define_file_perms, "%T.%s has `%s` defined, but it is not nor does it contain an `os.Handle` type.",
model_type, field.name, SUBTAG_PERMS, loc = loc)
}
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Map:
fmt.assertf(reflect.is_string(specific_type_info.key), "%T.%s is defined as a map[%T]. Only string types are currently supported as map keys.",
model_type,
field.name,
specific_type_info.key)
}
}
}
// Validate that all the required arguments are set and that the set arguments
// are up to the program's expectations.
@(optimization_mode="size")
validate_arguments :: proc(model: ^$T, parser: ^Parser) -> Error {
check_fields: for field, index in reflect.struct_fields_zipped(T) {
was_set := bit_array.get(&parser.fields_set, index)
field_name := get_field_name(field)
args_tag := reflect.struct_tag_get(field.tag, TAG_ARGS)
requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED)
required_min, required_max: int
has_requirements: bool
if is_required {
required_min, required_max, has_requirements = parse_requirements(requirement)
}
if has_requirements && required_min == 0 {
// Allow `0<n` or `<n` to bypass the required condition.
is_required = false
}
if _, is_array := field.type.variant.(runtime.Type_Info_Dynamic_Array); is_array && has_requirements {
// If it's an array, make sure it meets the required number of arguments.
ptr := cast(^runtime.Raw_Dynamic_Array)(cast(uintptr)model + field.offset)
if required_min == required_max - 1 && ptr.len != required_min {
return Validation_Error {
fmt.tprintf("The flag `%s` had %i option%s set, but it requires exactly %i.",
field_name,
ptr.len,
"" if ptr.len == 1 else "s",
required_min),
}
} else if required_min > ptr.len || ptr.len >= required_max {
if required_max == max(int) {
return Validation_Error {
fmt.tprintf("The flag `%s` had %i option%s set, but it requires at least %i.",
field_name,
ptr.len,
"" if ptr.len == 1 else "s",
required_min),
}
} else {
return Validation_Error {
fmt.tprintf("The flag `%s` had %i option%s set, but it requires at least %i and at most %i.",
field_name,
ptr.len,
"" if ptr.len == 1 else "s",
required_min,
required_max - 1),
}
}
}
} else if !was_set {
if is_required {
return Validation_Error {
fmt.tprintf("The required flag `%s` was not set.", field_name),
}
}
// Not set, not required; moving on.
continue
}
// All default checks have passed. The program gets a look at it now.
if global_custom_flag_checker != nil {
ptr := cast(rawptr)(cast(uintptr)model + field.offset)
error := global_custom_flag_checker(model,
field.name,
mem.make_any(ptr, field.type.id),
args_tag)
if len(error) > 0 {
// The program reported an error message.
return Validation_Error { error }
}
}
}
return nil
}

101
core/flags/parsing.odin Normal file
View File

@@ -0,0 +1,101 @@
package flags
@require import "core:container/bit_array"
@require import "core:fmt"
Parsing_Style :: enum {
// Odin-style: `-flag`, `-flag:option`, `-map:key=value`
Odin,
// UNIX-style: `-flag` or `--flag`, `--flag=argument`, `--flag argument repeating-argument`
Unix,
}
/*
Parse a slice of command-line arguments into an annotated struct.
*Allocates Using Provided Allocator*
By default, this proc will only allocate memory outside of its lifetime if it
has to append to a dynamic array, set a map value, or set a cstring.
The program is expected to free any allocations on `model` as a result of parsing.
Inputs:
- model: A pointer to an annotated struct with flag definitions.
- args: A slice of strings, usually `os.args[1:]`.
- style: The argument parsing style.
- validate_args: If `true`, will ensure that all required arguments are set if no errors occurred.
- strict: If `true`, will return on first error. Otherwise, parsing continues.
- allocator: (default: context.allocator)
- loc: The caller location for debugging purposes (default: #caller_location)
Returns:
- error: A union of errors; parsing, file open, a help request, or validation.
*/
@(optimization_mode="size")
parse :: proc(
model: ^$T,
args: []string,
style: Parsing_Style = .Odin,
validate_args: bool = true,
strict: bool = true,
allocator := context.allocator,
loc := #caller_location,
) -> (error: Error) {
context.allocator = allocator
validate_structure(model^, style, loc)
parser: Parser
defer {
bit_array.destroy(&parser.filled_pos)
bit_array.destroy(&parser.fields_set)
}
switch style {
case .Odin:
for arg in args {
error = parse_one_odin_arg(model, &parser, arg)
if strict && error != nil {
return
}
}
case .Unix:
// Support for `-flag argument (repeating-argument ...)`
future_args: int
current_flag: string
for i := 0; i < len(args); i += 1 {
#no_bounds_check arg := args[i]
future_args, current_flag, error = parse_one_unix_arg(model, &parser, arg)
if strict && error != nil {
return
}
for starting_future_args := future_args; future_args > 0; future_args -= 1 {
i += 1
if i == len(args) {
if future_args == starting_future_args {
return Parse_Error {
.No_Value,
fmt.tprintf("Expected a value for `%s` but none was given.", current_flag),
}
}
break
}
#no_bounds_check arg = args[i]
error = set_option(model, &parser, current_flag, arg)
if strict && error != nil {
return
}
}
}
}
if error == nil && validate_args {
return validate_arguments(model, &parser)
}
return
}

43
core/flags/rtti.odin Normal file
View File

@@ -0,0 +1,43 @@
package flags
import "base:runtime"
/*
Handle setting custom data types.
Inputs:
- data: A raw pointer to the field where the data will go.
- data_type: Type information on the underlying field.
- unparsed_value: The unparsed string that the flag is being set to.
- args_tag: The `args` tag from the struct's field.
Returns:
- error: An error message, or an empty string if no error occurred.
- handled: A boolean indicating if the setter handles this type.
- alloc_error: If an allocation error occurred, return it here.
*/
Custom_Type_Setter :: #type proc(
data: rawptr,
data_type: typeid,
unparsed_value: string,
args_tag: string,
) -> (
error: string,
handled: bool,
alloc_error: runtime.Allocator_Error,
)
@(private)
global_custom_type_setter: Custom_Type_Setter
/*
Set the global custom type setter.
Note that only one can be active at a time.
Inputs:
- setter: The type setter. Pass `nil` to disable any previously set setter.
*/
register_type_setter :: proc(setter: Custom_Type_Setter) {
global_custom_type_setter = setter
}

293
core/flags/usage.odin Normal file
View File

@@ -0,0 +1,293 @@
package flags
import "base:runtime"
import "core:fmt"
import "core:io"
import "core:reflect"
import "core:slice"
import "core:strconv"
import "core:strings"
/*
Write out the documentation for the command-line arguments to a stream.
Inputs:
- out: The stream to write to.
- data_type: The typeid of the data structure to describe.
- program: The name of the program, usually the first argument to `os.args`.
- style: The argument parsing style, required to show flags in the proper style.
*/
@(optimization_mode="size")
write_usage :: proc(out: io.Writer, data_type: typeid, program: string = "", style: Parsing_Style = .Odin) {
// All flags get their tags parsed so they can be reasoned about later.
Flag :: struct {
name: string,
usage: string,
type_description: string,
full_length: int,
pos: int,
required_min, required_max: int,
is_positional: bool,
is_required: bool,
is_boolean: bool,
is_variadic: bool,
variadic_length: int,
}
//
// POSITIONAL+REQUIRED, POSITIONAL, REQUIRED, NON_REQUIRED+NON_POSITIONAL, ...
//
sort_flags :: proc(i, j: Flag) -> slice.Ordering {
// `varg` goes to the end.
if i.name == INTERNAL_VARIADIC_FLAG {
return .Greater
} else if j.name == INTERNAL_VARIADIC_FLAG {
return .Less
}
// Handle positionals.
if i.is_positional {
if j.is_positional {
return slice.cmp(i.pos, j.pos)
} else {
return .Less
}
} else {
if j.is_positional {
return .Greater
}
}
// Then required flags.
if i.is_required {
if !j.is_required {
return .Less
}
} else if j.is_required {
return .Greater
}
// Finally, sort by name.
return slice.cmp(i.name, j.name)
}
describe_array_requirements :: proc(flag: Flag) -> (spec: string) {
if flag.is_required {
if flag.required_min == flag.required_max - 1 {
spec = fmt.tprintf(", exactly %i", flag.required_min)
} else if flag.required_min > 0 && flag.required_max == max(int) {
spec = fmt.tprintf(", at least %i", flag.required_min)
} else if flag.required_min == 0 && flag.required_max > 1 {
spec = fmt.tprintf(", at most %i", flag.required_max - 1)
} else if flag.required_min > 0 && flag.required_max > 1 {
spec = fmt.tprintf(", between %i and %i", flag.required_min, flag.required_max - 1)
} else {
spec = ", required"
}
}
return
}
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
flag_prefix, flag_assignment: string = ---, ---
switch style {
case .Odin: flag_prefix = "-"; flag_assignment = ":"
case .Unix: flag_prefix = "--"; flag_assignment = " "
}
visible_flags: [dynamic]Flag
defer delete(visible_flags)
longest_flag_length: int
for field in reflect.struct_fields_zipped(data_type) {
flag: Flag
if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
if _, is_hidden := get_struct_subtag(args_tag, SUBTAG_HIDDEN); is_hidden {
// Hidden flags stay hidden.
continue
}
if pos_str, is_pos := get_struct_subtag(args_tag, SUBTAG_POS); is_pos {
flag.is_positional = true
if pos, parse_ok := strconv.parse_u64_of_base(pos_str, 10); parse_ok {
flag.pos = cast(int)pos
}
}
if requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
flag.is_required = true
flag.required_min, flag.required_max, _ = parse_requirements(requirement)
}
if length_str, is_variadic := get_struct_subtag(args_tag, SUBTAG_VARIADIC); is_variadic {
flag.is_variadic = true
if length, parse_ok := strconv.parse_u64_of_base(length_str, 10); parse_ok {
flag.variadic_length = cast(int)length
}
}
}
flag.name = get_field_name(field)
flag.is_boolean = reflect.is_boolean(field.type)
if usage, ok := reflect.struct_tag_lookup(field.tag, TAG_USAGE); ok {
flag.usage = usage
} else {
flag.usage = UNDOCUMENTED_FLAG
}
#partial switch specific_type_info in field.type.variant {
case runtime.Type_Info_Map:
flag.type_description = fmt.tprintf("<%v>=<%v>%s",
specific_type_info.key.id,
specific_type_info.value.id,
", required" if flag.is_required else "")
case runtime.Type_Info_Dynamic_Array:
requirement_spec := describe_array_requirements(flag)
if flag.is_variadic || flag.name == INTERNAL_VARIADIC_FLAG {
if flag.variadic_length == 0 {
flag.type_description = fmt.tprintf("<%v, ...>%s",
specific_type_info.elem.id,
requirement_spec)
} else {
flag.type_description = fmt.tprintf("<%v, %i at once>%s",
specific_type_info.elem.id,
flag.variadic_length,
requirement_spec)
}
} else {
flag.type_description = fmt.tprintf("<%v>%s", specific_type_info.elem.id,
requirement_spec if len(requirement_spec) > 0 else ", multiple")
}
case:
if flag.is_boolean {
/*
if flag.is_required {
flag.type_description = ", required"
}
*/
} else {
flag.type_description = fmt.tprintf("<%v>%s",
field.type.id,
", required" if flag.is_required else "")
}
}
if flag.name == INTERNAL_VARIADIC_FLAG {
flag.full_length = len(flag.type_description)
} else if flag.is_boolean {
flag.full_length = len(flag_prefix) + len(flag.name) + len(flag.type_description)
} else {
flag.full_length = len(flag_prefix) + len(flag.name) + len(flag_assignment) + len(flag.type_description)
}
longest_flag_length = max(longest_flag_length, flag.full_length)
append(&visible_flags, flag)
}
slice.sort_by_cmp(visible_flags[:], sort_flags)
// All the flags have been figured out now.
if len(program) > 0 {
keep_it_short := len(visible_flags) >= ONE_LINE_FLAG_CUTOFF_COUNT
strings.write_string(&builder, "Usage:\n\t")
strings.write_string(&builder, program)
for flag in visible_flags {
if keep_it_short && !(flag.is_required || flag.is_positional || flag.name == INTERNAL_VARIADIC_FLAG) {
continue
}
strings.write_byte(&builder, ' ')
if flag.name == INTERNAL_VARIADIC_FLAG {
strings.write_string(&builder, "...")
continue
}
if !flag.is_required { strings.write_byte(&builder, '[') }
if !flag.is_positional { strings.write_string(&builder, flag_prefix) }
strings.write_string(&builder, flag.name)
if !flag.is_required { strings.write_byte(&builder, ']') }
}
strings.write_byte(&builder, '\n')
}
if len(visible_flags) == 0 {
// No visible flags. An unusual situation, but prevent any extra work.
fmt.wprint(out, strings.to_string(builder))
return
}
strings.write_string(&builder, "Flags:\n")
// Divide the positional/required arguments and the non-required arguments.
divider_index := -1
for flag, i in visible_flags {
if !flag.is_positional && !flag.is_required {
divider_index = i
break
}
}
if divider_index == 0 {
divider_index = -1
}
for flag, i in visible_flags {
if i == divider_index {
SPACING :: 2 // Number of spaces before the '|' from below.
strings.write_byte(&builder, '\t')
spacing := strings.repeat(" ", SPACING + longest_flag_length, context.temp_allocator)
strings.write_string(&builder, spacing)
strings.write_string(&builder, "|\n")
}
strings.write_byte(&builder, '\t')
if flag.name == INTERNAL_VARIADIC_FLAG {
strings.write_string(&builder, flag.type_description)
} else {
strings.write_string(&builder, flag_prefix)
strings.write_string(&builder, flag.name)
if !flag.is_boolean {
strings.write_string(&builder, flag_assignment)
}
strings.write_string(&builder, flag.type_description)
}
if strings.contains_rune(flag.usage, '\n') {
// Multi-line usage documentation. Let's make it look nice.
usage_builder := strings.builder_make(context.temp_allocator)
strings.write_byte(&usage_builder, '\n')
iter := strings.trim_space(flag.usage)
for line in strings.split_lines_iterator(&iter) {
strings.write_string(&usage_builder, "\t\t")
strings.write_string(&usage_builder, strings.trim_left_space(line))
strings.write_byte(&usage_builder, '\n')
}
strings.write_string(&builder, strings.to_string(usage_builder))
} else {
// Single-line usage documentation.
spacing := strings.repeat(" ",
(longest_flag_length) - flag.full_length,
context.temp_allocator)
strings.write_string(&builder, spacing)
strings.write_string(&builder, " | ")
strings.write_string(&builder, flag.usage)
strings.write_byte(&builder, '\n')
}
}
fmt.wprint(out, strings.to_string(builder))
}

130
core/flags/util.odin Normal file
View File

@@ -0,0 +1,130 @@
package flags
import "core:fmt"
@require import "core:os"
@require import "core:path/filepath"
import "core:strings"
/*
Parse any arguments into an annotated struct or exit if there was an error.
*Allocates Using Provided Allocator*
This is a convenience wrapper over `parse` and `print_errors`.
Inputs:
- model: A pointer to an annotated struct.
- program_args: A slice of strings, usually `os.args`.
- style: The argument parsing style.
- allocator: (default: context.allocator)
- loc: The caller location for debugging purposes (default: #caller_location)
*/
@(optimization_mode="size")
parse_or_exit :: proc(
model: ^$T,
program_args: []string,
style: Parsing_Style = .Odin,
allocator := context.allocator,
loc := #caller_location,
) {
assert(len(program_args) > 0, "Program arguments slice is empty.", loc)
program := filepath.base(program_args[0])
args: []string
if len(program_args) > 1 {
args = program_args[1:]
}
error := parse(model, args, style)
if error != nil {
stderr := os.stream_from_handle(os.stderr)
if len(args) == 0 {
// No arguments entered, and there was an error; show the usage,
// specifically on STDERR.
write_usage(stderr, T, program, style)
fmt.wprintln(stderr)
}
print_errors(T, error, program, style)
_, was_help_request := error.(Help_Request)
os.exit(0 if was_help_request else 1)
}
}
/*
Print out any errors that may have resulted from parsing.
All error messages print to STDERR, while usage goes to STDOUT, if requested.
Inputs:
- data_type: The typeid of the data structure to describe, if usage is requested.
- error: The error returned from `parse`.
- style: The argument parsing style, required to show flags in the proper style, when usage is shown.
*/
@(optimization_mode="size")
print_errors :: proc(data_type: typeid, error: Error, program: string, style: Parsing_Style = .Odin) {
stderr := os.stream_from_handle(os.stderr)
stdout := os.stream_from_handle(os.stdout)
switch specific_error in error {
case Parse_Error:
fmt.wprintfln(stderr, "[%T.%v] %s", specific_error, specific_error.reason, specific_error.message)
case Open_File_Error:
fmt.wprintfln(stderr, "[%T#%i] Unable to open file with perms 0o%o in mode 0x%x: %s",
specific_error,
specific_error.errno,
specific_error.perms,
specific_error.mode,
specific_error.filename)
case Validation_Error:
fmt.wprintfln(stderr, "[%T] %s", specific_error, specific_error.message)
case Help_Request:
write_usage(stdout, data_type, program, style)
}
}
/*
Get the value for a subtag.
This is useful if you need to parse through the `args` tag for a struct field
on a custom type setter or custom flag checker.
Example:
import "core:flags"
import "core:fmt"
subtag_example :: proc() {
args_tag := "precision=3,signed"
precision, has_precision := flags.get_subtag(args_tag, "precision")
signed, is_signed := flags.get_subtag(args_tag, "signed")
fmt.printfln("precision = %q, %t", precision, has_precision)
fmt.printfln("signed = %q, %t", signed, is_signed)
}
Output:
precision = "3", true
signed = "", true
*/
get_subtag :: proc(tag, id: string) -> (value: string, ok: bool) {
// This proc was initially private in `internal_rtti.odin`, but given how
// useful it would be to custom type setters and flag checkers, it lives
// here now.
tag := tag
for subtag in strings.split_iterator(&tag, ",") {
if equals := strings.index_byte(subtag, '='); equals != -1 && id == subtag[:equals] {
return subtag[1 + equals:], true
} else if id == subtag {
return "", true
}
}
return
}

View File

@@ -0,0 +1,37 @@
package flags
/*
Check a flag after parsing, during the validation stage.
Inputs:
- model: A raw pointer to the data structure provided to `parse`.
- name: The name of the flag being checked.
- value: An `any` type that contains the value to be checked.
- args_tag: The `args` tag from within the struct.
Returns:
- error: An error message, or an empty string if no error occurred.
*/
Custom_Flag_Checker :: #type proc(
model: rawptr,
name: string,
value: any,
args_tag: string,
) -> (
error: string,
)
@(private)
global_custom_flag_checker: Custom_Flag_Checker
/*
Set the global custom flag checker.
Note that only one can be active at a time.
Inputs:
- checker: The flag checker. Pass `nil` to disable any previously set checker.
*/
register_flag_checker :: proc(checker: Custom_Flag_Checker) {
global_custom_flag_checker = checker
}

View File

@@ -1973,11 +1973,13 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
// fi.hash = false;
fi.indent += 1
if !is_soa && hash {
is_empty := len(info.names) == 0
if !is_soa && hash && !is_empty {
io.write_byte(fi.writer, '\n', &fi.n)
}
defer {
if hash {
if !is_soa && hash && !is_empty {
for _ in 0..<indent { io.write_byte(fi.writer, '\t', &fi.n) }
}
io.write_byte(fi.writer, ']' if is_soa && the_verb == 'v' else '}', &fi.n)
@@ -2025,9 +2027,9 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
}
io.write_string(fi.writer, base_type_name, &fi.n)
io.write_byte(fi.writer, '{', &fi.n)
if hash { io.write_byte(fi.writer, '\n', &fi.n) }
if hash && !is_empty { io.write_byte(fi.writer, '\n', &fi.n) }
defer {
if hash {
if hash && !is_empty {
fi.indent -= 1
fmt_write_indent(fi)
fi.indent += 1
@@ -2075,6 +2077,10 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
if hash { io.write_string(fi.writer, ",\n", &fi.n) }
}
}
if hash && n > 0 {
for _ in 0..<indent { io.write_byte(fi.writer, '\t', &fi.n) }
}
} else {
field_count := -1
for name, i in info.names {

View File

@@ -362,11 +362,11 @@ platform_count_lsb :: #force_inline proc(a: $T) -> (count: int)
count_lsb :: proc { int_count_lsb, platform_count_lsb, }
int_random_digit :: proc(r: ^rnd.Rand = nil) -> (res: DIGIT) {
int_random_digit :: proc() -> (res: DIGIT) {
when _DIGIT_BITS == 60 { // DIGIT = u64
return DIGIT(rnd.uint64(r)) & _MASK
return DIGIT(rnd.uint64()) & _MASK
} else when _DIGIT_BITS == 28 { // DIGIT = u32
return DIGIT(rnd.uint32(r)) & _MASK
return DIGIT(rnd.uint32()) & _MASK
} else {
panic("Unsupported DIGIT size.")
}
@@ -374,12 +374,12 @@ int_random_digit :: proc(r: ^rnd.Rand = nil) -> (res: DIGIT) {
return 0 // We shouldn't get here.
}
int_random :: proc(dest: ^Int, bits: int, r: ^rnd.Rand = nil, allocator := context.allocator) -> (err: Error) {
int_random :: proc(dest: ^Int, bits: int, allocator := context.allocator) -> (err: Error) {
/*
Check that `a` is usable.
*/
assert_if_nil(dest)
return #force_inline internal_int_random(dest, bits, r, allocator)
return #force_inline internal_int_random(dest, bits, allocator)
}
random :: proc { int_random, }

View File

@@ -2178,15 +2178,20 @@ internal_int_grow :: proc(a: ^Int, digits: int, allow_shrink := false, allocator
}
/*
If not yet iniialized, initialize the `digit` backing with the allocator we were passed.
If not yet initialized, initialize the `digit` backing with the allocator we were passed.
*/
if cap == 0 {
a.digit = make([dynamic]DIGIT, needed, allocator)
} else if cap != needed {
} else if cap < needed {
/*
`[dynamic]DIGIT` already knows what allocator was used for it, so resize will do the right thing.
*/
resize(&a.digit, needed)
} else if cap > needed {
/*
Same applies to builtin.shrink here as resize above
*/
builtin.shrink(&a.digit, needed)
}
/*
Let's see if the allocation/resize worked as expected.
@@ -2812,11 +2817,11 @@ internal_platform_count_lsb :: #force_inline proc(a: $T) -> (count: int)
internal_count_lsb :: proc { internal_int_count_lsb, internal_platform_count_lsb, }
internal_int_random_digit :: proc(r: ^rnd.Rand = nil) -> (res: DIGIT) {
internal_int_random_digit :: proc() -> (res: DIGIT) {
when _DIGIT_BITS == 60 { // DIGIT = u64
return DIGIT(rnd.uint64(r)) & _MASK
return DIGIT(rnd.uint64()) & _MASK
} else when _DIGIT_BITS == 28 { // DIGIT = u32
return DIGIT(rnd.uint32(r)) & _MASK
return DIGIT(rnd.uint32()) & _MASK
} else {
panic("Unsupported DIGIT size.")
}
@@ -2824,7 +2829,7 @@ internal_int_random_digit :: proc(r: ^rnd.Rand = nil) -> (res: DIGIT) {
return 0 // We shouldn't get here.
}
internal_int_random :: proc(dest: ^Int, bits: int, r: ^rnd.Rand = nil, allocator := context.allocator) -> (err: Error) {
internal_int_random :: proc(dest: ^Int, bits: int, allocator := context.allocator) -> (err: Error) {
context.allocator = allocator
bits := bits
@@ -2841,7 +2846,7 @@ internal_int_random :: proc(dest: ^Int, bits: int, r: ^rnd.Rand = nil, allocator
#force_inline internal_grow(dest, digits) or_return
for i := 0; i < digits; i += 1 {
dest.digit[i] = int_random_digit(r) & _MASK
dest.digit[i] = int_random_digit() & _MASK
}
if bits > 0 {
dest.digit[digits - 1] &= ((1 << uint(bits)) - 1)

View File

@@ -12,8 +12,6 @@
package math_big
import rnd "core:math/rand"
/*
Determines if an Integer is divisible by one of the _PRIME_TABLE primes.
Returns true if it is, false if not.
@@ -315,7 +313,7 @@ internal_int_prime_miller_rabin :: proc(a, b: ^Int, allocator := context.allocat
Assumes `a` not to be `nil` and to have been initialized.
*/
internal_int_is_prime :: proc(a: ^Int, miller_rabin_trials := int(-1), miller_rabin_only := USE_MILLER_RABIN_ONLY, r: ^rnd.Rand = nil, allocator := context.allocator) -> (is_prime: bool, err: Error) {
internal_int_is_prime :: proc(a: ^Int, miller_rabin_trials := int(-1), miller_rabin_only := USE_MILLER_RABIN_ONLY, allocator := context.allocator) -> (is_prime: bool, err: Error) {
context.allocator = allocator
miller_rabin_trials := miller_rabin_trials
@@ -461,7 +459,7 @@ internal_int_is_prime :: proc(a: ^Int, miller_rabin_trials := int(-1), miller_ra
for ix := 0; ix < miller_rabin_trials; ix += 1 {
// rand() guarantees the first digit to be non-zero
internal_random(b, _DIGIT_TYPE_BITS, r) or_return
internal_random(b, _DIGIT_TYPE_BITS) or_return
// Reduce digit before casting because DIGIT might be bigger than
// an unsigned int and "mask" on the other side is most probably not.
@@ -1183,7 +1181,7 @@ internal_int_prime_next_prime :: proc(a: ^Int, trials: int, bbs_style: bool, all
This is possibly the mother of all prime generation functions, muahahahahaha!
*/
internal_random_prime :: proc(a: ^Int, size_in_bits: int, trials: int, flags := Primality_Flags{}, r: ^rnd.Rand = nil, allocator := context.allocator) -> (err: Error) {
internal_random_prime :: proc(a: ^Int, size_in_bits: int, trials: int, flags := Primality_Flags{}, allocator := context.allocator) -> (err: Error) {
context.allocator = allocator
flags := flags
trials := trials

View File

@@ -8,12 +8,12 @@ float32_uniform :: float32_range
// Triangular Distribution
// See: http://wikipedia.org/wiki/Triangular_distribution
@(require_results)
float64_triangular :: proc(lo, hi: f64, mode: Maybe(f64), r: ^Rand = nil) -> f64 {
float64_triangular :: proc(lo, hi: f64, mode: Maybe(f64)) -> f64 {
if hi-lo == 0 {
return lo
}
lo, hi := lo, hi
u := float64(r)
u := float64()
c := f64(0.5) if mode == nil else clamp((mode.?-lo) / (hi-lo), 0, 1)
if u > c {
u = 1-u
@@ -26,12 +26,12 @@ float64_triangular :: proc(lo, hi: f64, mode: Maybe(f64), r: ^Rand = nil) -> f64
// Triangular Distribution
// See: http://wikipedia.org/wiki/Triangular_distribution
@(require_results)
float32_triangular :: proc(lo, hi: f32, mode: Maybe(f32), r: ^Rand = nil) -> f32 {
float32_triangular :: proc(lo, hi: f32, mode: Maybe(f32)) -> f32 {
if hi-lo == 0 {
return lo
}
lo, hi := lo, hi
u := float32(r)
u := float32()
c := f32(0.5) if mode == nil else clamp((mode.?-lo) / (hi-lo), 0, 1)
if u > c {
u = 1-u
@@ -44,25 +44,25 @@ float32_triangular :: proc(lo, hi: f32, mode: Maybe(f32), r: ^Rand = nil) -> f32
// Normal/Gaussian Distribution
@(require_results)
float64_normal :: proc(mean, stddev: f64, r: ^Rand = nil) -> f64 {
return norm_float64(r) * stddev + mean
float64_normal :: proc(mean, stddev: f64) -> f64 {
return norm_float64() * stddev + mean
}
// Normal/Gaussian Distribution
@(require_results)
float32_normal :: proc(mean, stddev: f32, r: ^Rand = nil) -> f32 {
return f32(float64_normal(f64(mean), f64(stddev), r))
float32_normal :: proc(mean, stddev: f32) -> f32 {
return f32(float64_normal(f64(mean), f64(stddev)))
}
// Log Normal Distribution
@(require_results)
float64_log_normal :: proc(mean, stddev: f64, r: ^Rand = nil) -> f64 {
return math.exp(float64_normal(mean, stddev, r))
float64_log_normal :: proc(mean, stddev: f64) -> f64 {
return math.exp(float64_normal(mean, stddev))
}
// Log Normal Distribution
@(require_results)
float32_log_normal :: proc(mean, stddev: f32, r: ^Rand = nil) -> f32 {
return f32(float64_log_normal(f64(mean), f64(stddev), r))
float32_log_normal :: proc(mean, stddev: f32) -> f32 {
return f32(float64_log_normal(f64(mean), f64(stddev)))
}
@@ -72,8 +72,8 @@ float32_log_normal :: proc(mean, stddev: f32, r: ^Rand = nil) -> f32 {
// 0 to positive infinity if lambda > 0
// negative infinity to 0 if lambda <= 0
@(require_results)
float64_exponential :: proc(lambda: f64, r: ^Rand = nil) -> f64 {
return - math.ln(1 - float64(r)) / lambda
float64_exponential :: proc(lambda: f64) -> f64 {
return - math.ln(1 - float64()) / lambda
}
// Exponential Distribution
// `lambda` is 1.0/(desired mean). It should be non-zero.
@@ -81,8 +81,8 @@ float64_exponential :: proc(lambda: f64, r: ^Rand = nil) -> f64 {
// 0 to positive infinity if lambda > 0
// negative infinity to 0 if lambda <= 0
@(require_results)
float32_exponential :: proc(lambda: f32, r: ^Rand = nil) -> f32 {
return f32(float64_exponential(f64(lambda), r))
float32_exponential :: proc(lambda: f32) -> f32 {
return f32(float64_exponential(f64(lambda)))
}
@@ -96,7 +96,7 @@ float32_exponential :: proc(lambda: f32, r: ^Rand = nil) -> f32 {
//
// mean is alpha*beta, variance is math.pow(alpha*beta, 2)
@(require_results)
float64_gamma :: proc(alpha, beta: f64, r: ^Rand = nil) -> f64 {
float64_gamma :: proc(alpha, beta: f64) -> f64 {
if alpha <= 0 || beta <= 0 {
panic(#procedure + ": alpha and beta must be > 0.0")
}
@@ -112,11 +112,11 @@ float64_gamma :: proc(alpha, beta: f64, r: ^Rand = nil) -> f64 {
bbb := alpha - LOG4
ccc := alpha + ainv
for {
u1 := float64(r)
u1 := float64()
if !(1e-7 < u1 && u1 < 0.9999999) {
continue
}
u2 := 1 - float64(r)
u2 := 1 - float64()
v := math.ln(u1 / (1 - u1)) / ainv
x := alpha * math.exp(v)
z := u1 * u1 * u2
@@ -127,12 +127,12 @@ float64_gamma :: proc(alpha, beta: f64, r: ^Rand = nil) -> f64 {
}
case alpha == 1:
// float64_exponential(1/beta)
return -math.ln(1 - float64(r)) * beta
return -math.ln(1 - float64()) * beta
case:
// ALGORITHM GS of Statistical Computing - Kennedy & Gentle
x: f64
for {
u := float64(r)
u := float64()
b := (math.e + alpha) / math.e
p := b * u
if p <= 1 {
@@ -140,7 +140,7 @@ float64_gamma :: proc(alpha, beta: f64, r: ^Rand = nil) -> f64 {
} else {
x = -math.ln((b - p) / alpha)
}
u1 := float64(r)
u1 := float64()
if p > 1 {
if u1 <= math.pow(x, alpha-1) {
break
@@ -162,8 +162,8 @@ float64_gamma :: proc(alpha, beta: f64, r: ^Rand = nil) -> f64 {
//
// mean is alpha*beta, variance is math.pow(alpha*beta, 2)
@(require_results)
float32_gamma :: proc(alpha, beta: f32, r: ^Rand = nil) -> f32 {
return f32(float64_gamma(f64(alpha), f64(beta), r))
float32_gamma :: proc(alpha, beta: f32) -> f32 {
return f32(float64_gamma(f64(alpha), f64(beta)))
}
@@ -173,14 +173,14 @@ float32_gamma :: proc(alpha, beta: f32, r: ^Rand = nil) -> f32 {
//
// Return values range between 0 and 1
@(require_results)
float64_beta :: proc(alpha, beta: f64, r: ^Rand = nil) -> f64 {
float64_beta :: proc(alpha, beta: f64) -> f64 {
if alpha <= 0 || beta <= 0 {
panic(#procedure + ": alpha and beta must be > 0.0")
}
// Knuth Vol 2 Ed 3 pg 134 "the beta distribution"
y := float64_gamma(alpha, 1.0, r)
y := float64_gamma(alpha, 1.0)
if y != 0 {
return y / (y + float64_gamma(beta, 1.0, r))
return y / (y + float64_gamma(beta, 1.0))
}
return 0
}
@@ -190,35 +190,35 @@ float64_beta :: proc(alpha, beta: f64, r: ^Rand = nil) -> f64 {
//
// Return values range between 0 and 1
@(require_results)
float32_beta :: proc(alpha, beta: f32, r: ^Rand = nil) -> f32 {
return f32(float64_beta(f64(alpha), f64(beta), r))
float32_beta :: proc(alpha, beta: f32) -> f32 {
return f32(float64_beta(f64(alpha), f64(beta)))
}
// Pareto distribution, `alpha` is the shape parameter.
// https://wikipedia.org/wiki/Pareto_distribution
@(require_results)
float64_pareto :: proc(alpha: f64, r: ^Rand = nil) -> f64 {
return math.pow(1 - float64(r), -1.0 / alpha)
float64_pareto :: proc(alpha: f64) -> f64 {
return math.pow(1 - float64(), -1.0 / alpha)
}
// Pareto distribution, `alpha` is the shape parameter.
// https://wikipedia.org/wiki/Pareto_distribution
@(require_results)
float32_pareto :: proc(alpha, beta: f32, r: ^Rand = nil) -> f32 {
return f32(float64_pareto(f64(alpha), r))
float32_pareto :: proc(alpha, beta: f32) -> f32 {
return f32(float64_pareto(f64(alpha)))
}
// Weibull distribution, `alpha` is the scale parameter, `beta` is the shape parameter.
@(require_results)
float64_weibull :: proc(alpha, beta: f64, r: ^Rand = nil) -> f64 {
u := 1 - float64(r)
float64_weibull :: proc(alpha, beta: f64) -> f64 {
u := 1 - float64()
return alpha * math.pow(-math.ln(u), 1.0/beta)
}
// Weibull distribution, `alpha` is the scale parameter, `beta` is the shape parameter.
@(require_results)
float32_weibull :: proc(alpha, beta: f32, r: ^Rand = nil) -> f32 {
return f32(float64_weibull(f64(alpha), f64(beta), r))
float32_weibull :: proc(alpha, beta: f32) -> f32 {
return f32(float64_weibull(f64(alpha), f64(beta)))
}
@@ -227,23 +227,23 @@ float32_weibull :: proc(alpha, beta: f32, r: ^Rand = nil) -> f32 {
// `kappa` is the concentration parameter which must be >= 0
// When `kappa` is zero, the Distribution is a uniform Distribution over the range 0 to 2pi
@(require_results)
float64_von_mises :: proc(mean_angle, kappa: f64, r: ^Rand = nil) -> f64 {
float64_von_mises :: proc(mean_angle, kappa: f64) -> f64 {
// Fisher, N.I., "Statistical Analysis of Circular Data", Cambridge University Press, 1993.
mu := mean_angle
if kappa <= 1e-6 {
return math.TAU * float64(r)
return math.TAU * float64()
}
s := 0.5 / kappa
t := s + math.sqrt(1 + s*s)
z: f64
for {
u1 := float64(r)
u1 := float64()
z = math.cos(math.TAU * 0.5 * u1)
d := z / (t + z)
u2 := float64(r)
u2 := float64()
if u2 < 1 - d*d || u2 <= (1-d)*math.exp(d) {
break
}
@@ -251,7 +251,7 @@ float64_von_mises :: proc(mean_angle, kappa: f64, r: ^Rand = nil) -> f64 {
q := 1.0 / t
f := (q + z) / (1 + q*z)
u3 := float64(r)
u3 := float64()
if u3 > 0.5 {
return math.mod(mu + math.acos(f), math.TAU)
} else {
@@ -263,57 +263,57 @@ float64_von_mises :: proc(mean_angle, kappa: f64, r: ^Rand = nil) -> f64 {
// `kappa` is the concentration parameter which must be >= 0
// When `kappa` is zero, the Distribution is a uniform Distribution over the range 0 to 2pi
@(require_results)
float32_von_mises :: proc(mean_angle, kappa: f32, r: ^Rand = nil) -> f32 {
return f32(float64_von_mises(f64(mean_angle), f64(kappa), r))
float32_von_mises :: proc(mean_angle, kappa: f32) -> f32 {
return f32(float64_von_mises(f64(mean_angle), f64(kappa)))
}
// Cauchy-Lorentz Distribution
// `x_0` is the location, `gamma` is the scale where `gamma` > 0
@(require_results)
float64_cauchy_lorentz :: proc(x_0, gamma: f64, r: ^Rand = nil) -> f64 {
float64_cauchy_lorentz :: proc(x_0, gamma: f64) -> f64 {
assert(gamma > 0)
// Calculated from the inverse CDF
return math.tan(math.PI * (float64(r) - 0.5))*gamma + x_0
return math.tan(math.PI * (float64() - 0.5))*gamma + x_0
}
// Cauchy-Lorentz Distribution
// `x_0` is the location, `gamma` is the scale where `gamma` > 0
@(require_results)
float32_cauchy_lorentz :: proc(x_0, gamma: f32, r: ^Rand = nil) -> f32 {
return f32(float64_cauchy_lorentz(f64(x_0), f64(gamma), r))
float32_cauchy_lorentz :: proc(x_0, gamma: f32) -> f32 {
return f32(float64_cauchy_lorentz(f64(x_0), f64(gamma)))
}
// Log Cauchy-Lorentz Distribution
// `x_0` is the location, `gamma` is the scale where `gamma` > 0
@(require_results)
float64_log_cauchy_lorentz :: proc(x_0, gamma: f64, r: ^Rand = nil) -> f64 {
float64_log_cauchy_lorentz :: proc(x_0, gamma: f64) -> f64 {
assert(gamma > 0)
return math.exp(math.tan(math.PI * (float64(r) - 0.5))*gamma + x_0)
return math.exp(math.tan(math.PI * (float64() - 0.5))*gamma + x_0)
}
// Log Cauchy-Lorentz Distribution
// `x_0` is the location, `gamma` is the scale where `gamma` > 0
@(require_results)
float32_log_cauchy_lorentz :: proc(x_0, gamma: f32, r: ^Rand = nil) -> f32 {
return f32(float64_log_cauchy_lorentz(f64(x_0), f64(gamma), r))
float32_log_cauchy_lorentz :: proc(x_0, gamma: f32) -> f32 {
return f32(float64_log_cauchy_lorentz(f64(x_0), f64(gamma)))
}
// Laplace Distribution
// `b` is the scale where `b` > 0
@(require_results)
float64_laplace :: proc(mean, b: f64, r: ^Rand = nil) -> f64 {
float64_laplace :: proc(mean, b: f64) -> f64 {
assert(b > 0)
p := float64(r)-0.5
p := float64()-0.5
return -math.sign(p)*math.ln(1 - 2*abs(p))*b + mean
}
// Laplace Distribution
// `b` is the scale where `b` > 0
@(require_results)
float32_laplace :: proc(mean, b: f32, r: ^Rand = nil) -> f32 {
return f32(float64_laplace(f64(mean), f64(b), r))
float32_laplace :: proc(mean, b: f32) -> f32 {
return f32(float64_laplace(f64(mean), f64(b)))
}
@@ -321,18 +321,18 @@ float32_laplace :: proc(mean, b: f32, r: ^Rand = nil) -> f32 {
// `eta` is the shape, `b` is the scale
// Both `eta` and `b` must be > 0
@(require_results)
float64_gompertz :: proc(eta, b: f64, r: ^Rand = nil) -> f64 {
float64_gompertz :: proc(eta, b: f64) -> f64 {
if eta <= 0 || b <= 0 {
panic(#procedure + ": eta and b must be > 0.0")
}
p := float64(r)
p := float64()
return math.ln(1 - math.ln(1 - p)/eta)/b
}
// Gompertz Distribution
// `eta` is the shape, `b` is the scale
// Both `eta` and `b` must be > 0
@(require_results)
float32_gompertz :: proc(eta, b: f32, r: ^Rand = nil) -> f32 {
return f32(float64_gompertz(f64(eta), f64(b), r))
float32_gompertz :: proc(eta, b: f32) -> f32 {
return f32(float64_gompertz(f64(eta), f64(b)))
}

View File

@@ -16,7 +16,7 @@ import "core:math"
// https://www.jstatsoft.org/article/view/v005i08 [web page]
//
@(require_results)
exp_float64 :: proc(r: ^Rand = nil) -> f64 {
exp_float64 :: proc() -> f64 {
re :: 7.69711747013104972
@(static, rodata)
@@ -199,16 +199,16 @@ exp_float64 :: proc(r: ^Rand = nil) -> f64 {
}
for {
j := uint32(r)
j := uint32()
i := j & 0xFF
x := f64(j) * f64(we[i])
if j < ke[i] {
return x
}
if i == 0 {
return re - math.ln(float64(r))
return re - math.ln(float64())
}
if fe[i]+f32(float64(r))*(fe[i-1]-fe[i]) < f32(math.exp(-x)) {
if fe[i]+f32(float64())*(fe[i-1]-fe[i]) < f32(math.exp(-x)) {
return x
}
}

View File

@@ -18,7 +18,7 @@ import "core:math"
// https://www.jstatsoft.org/article/view/v005i08 [web page]
//
@(require_results)
norm_float64 :: proc(r: ^Rand = nil) -> f64 {
norm_float64 :: proc() -> f64 {
rn :: 3.442619855899
@(static, rodata)
@@ -115,15 +115,8 @@ norm_float64 :: proc(r: ^Rand = nil) -> f64 {
0.008624485, 0.005548995, 0.0026696292,
}
r := r
if r == nil {
// NOTE(bill, 2020-09-07): Do this so that people can
// enforce the global random state if necessary with `nil`
r = &global_rand
}
for {
j := i32(uint32(r))
j := i32(uint32())
i := j & 0x7f
x := f64(j) * f64(wn[i])
if u32(abs(j)) < kn[i] {
@@ -133,15 +126,15 @@ norm_float64 :: proc(r: ^Rand = nil) -> f64 {
if i == 0 {
for {
x = -math.ln(float64(r)) * (1.0/ rn)
y := -math.ln(float64(r))
x = -math.ln(float64()) * (1.0/ rn)
y := -math.ln(float64())
if y+y >= x*x {
break
}
}
return j > 0 ? rn + x : -rn - x
}
if fn[i]+f32(float64(r))*(fn[i-1]-fn[i]) < f32(math.exp(-0.5*x*x)) {
if fn[i]+f32(float64())*(fn[i-1]-fn[i]) < f32(math.exp(-0.5*x*x)) {
return x
}
}

View File

@@ -5,22 +5,22 @@ Package core:math/rand implements various random number generators
package rand
import "base:intrinsics"
import "core:crypto"
import "base:runtime"
import "core:math"
import "core:mem"
Rand :: struct {
state: u64,
inc: u64,
is_system: bool,
Default_Random_State :: runtime.Default_Random_State
default_random_generator :: runtime.default_random_generator
create :: proc(seed: u64) -> (state: Default_Random_State) {
seed := seed
runtime.default_random_generator(&state)
runtime.default_random_generator_proc(&state, .Reset, ([^]byte)(&seed)[:size_of(seed)])
return
}
@(private)
global_rand := create(u64(intrinsics.read_cycle_counter()))
/*
Sets the seed used by the global random number generator.
Reset the seed used by the context.random_generator.
Inputs:
- seed: The seed value
@@ -37,139 +37,46 @@ Example:
Possible Output:
10
*/
@(deprecated="Prefer `rand.reset`")
set_global_seed :: proc(seed: u64) {
init(&global_rand, seed)
runtime.random_generator_reset_u64(context.random_generator, seed)
}
/*
Creates a new random number generator.
Reset the seed used by the context.random_generator.
Inputs:
- seed: The seed value to create the random number generator with
Returns:
- res: The created random number generator
- seed: The seed value
Example:
import "core:math/rand"
import "core:fmt"
create_example :: proc() {
my_rand := rand.create(1)
fmt.println(rand.uint64(&my_rand))
set_global_seed_example :: proc() {
rand.set_global_seed(1)
fmt.println(rand.uint64())
}
Possible Output:
10
*/
@(require_results)
create :: proc(seed: u64) -> (res: Rand) {
r: Rand
init(&r, seed)
return r
reset :: proc(seed: u64) {
runtime.random_generator_reset_u64(context.random_generator, seed)
}
/*
Initialises a random number generator.
Inputs:
- r: The random number generator to initialise
- seed: The seed value to initialise this random number generator
Example:
import "core:math/rand"
import "core:fmt"
init_example :: proc() {
my_rand: rand.Rand
rand.init(&my_rand, 1)
fmt.println(rand.uint64(&my_rand))
}
Possible Output:
10
*/
init :: proc(r: ^Rand, seed: u64) {
r.state = 0
r.inc = (seed << 1) | 1
_random_u64(r)
r.state += seed
_random_u64(r)
}
/*
Initialises a random number generator to use the system random number generator.
The system random number generator is platform specific, and not supported
on all targets.
Inputs:
- r: The random number generator to use the system random number generator
WARNING: Panics if the system random number generator is not supported.
Support can be determined via the `core:crypto.HAS_RAND_BYTES` constant.
Example:
import "core:crypto"
import "core:math/rand"
import "core:fmt"
init_as_system_example :: proc() {
my_rand: rand.Rand
switch crypto.HAS_RAND_BYTES {
case true:
rand.init_as_system(&my_rand)
fmt.println(rand.uint64(&my_rand))
case false:
fmt.println("system random not supported!")
}
}
Possible Output:
10
*/
init_as_system :: proc(r: ^Rand) {
if !crypto.HAS_RAND_BYTES {
panic(#procedure + " is not supported on this platform yet")
}
r.state = 0
r.inc = 0
r.is_system = true
}
@(private)
_random_u64 :: proc(r: ^Rand) -> u64 {
r := r
switch {
case r == nil:
r = &global_rand
case r.is_system:
value: u64
crypto.rand_bytes((cast([^]u8)&value)[:size_of(u64)])
return value
}
old_state := r.state
r.state = old_state * 6364136223846793005 + (r.inc|1)
xor_shifted := (((old_state >> 59) + 5) ~ old_state) * 12605985483714917081
rot := (old_state >> 59)
return (xor_shifted >> rot) | (xor_shifted << ((-rot) & 63))
_random_u64 :: proc() -> (res: u64) {
ok := runtime.random_generator_read_ptr(context.random_generator, &res, size_of(res))
assert(ok, "uninitialized context.random_generator")
return
}
/*
Generates a random 32 bit value using the provided random number generator. If no generator is provided the global random number generator will be used.
Inputs:
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random unsigned 32 bit value
@@ -178,11 +85,7 @@ Example:
import "core:fmt"
uint32_example :: proc() {
// Using the global random number generator
fmt.println(rand.uint32())
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.uint32(&my_rand))
}
Possible Output:
@@ -192,14 +95,11 @@ Possible Output:
*/
@(require_results)
uint32 :: proc(r: ^Rand = nil) -> (val: u32) { return u32(_random_u64(r)) }
uint32 :: proc() -> (val: u32) { return u32(_random_u64()) }
/*
Generates a random 64 bit value using the provided random number generator. If no generator is provided the global random number generator will be used.
Inputs:
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random unsigned 64 bit value
@@ -208,11 +108,7 @@ Example:
import "core:fmt"
uint64_example :: proc() {
// Using the global random number generator
fmt.println(rand.uint64())
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.uint64(&my_rand))
}
Possible Output:
@@ -222,14 +118,11 @@ Possible Output:
*/
@(require_results)
uint64 :: proc(r: ^Rand = nil) -> (val: u64) { return _random_u64(r) }
uint64 :: proc() -> (val: u64) { return _random_u64() }
/*
Generates a random 128 bit value using the provided random number generator. If no generator is provided the global random number generator will be used.
Inputs:
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random unsigned 128 bit value
@@ -238,11 +131,7 @@ Example:
import "core:fmt"
uint128_example :: proc() {
// Using the global random number generator
fmt.println(rand.uint128())
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.uint128(&my_rand))
}
Possible Output:
@@ -252,9 +141,9 @@ Possible Output:
*/
@(require_results)
uint128 :: proc(r: ^Rand = nil) -> (val: u128) {
a := u128(_random_u64(r))
b := u128(_random_u64(r))
uint128 :: proc() -> (val: u128) {
a := u128(_random_u64())
b := u128(_random_u64())
return (a<<64) | b
}
@@ -262,9 +151,6 @@ uint128 :: proc(r: ^Rand = nil) -> (val: u128) {
Generates a random 31 bit value using the provided random number generator. If no generator is provided the global random number generator will be used.
The sign bit will always be set to 0, thus all generated numbers will be positive.
Inputs:
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random 31 bit value
@@ -273,11 +159,7 @@ Example:
import "core:fmt"
int31_example :: proc() {
// Using the global random number generator
fmt.println(rand.int31())
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.int31(&my_rand))
}
Possible Output:
@@ -286,15 +168,12 @@ Possible Output:
389
*/
@(require_results) int31 :: proc(r: ^Rand = nil) -> (val: i32) { return i32(uint32(r) << 1 >> 1) }
@(require_results) int31 :: proc() -> (val: i32) { return i32(uint32() << 1 >> 1) }
/*
Generates a random 63 bit value using the provided random number generator. If no generator is provided the global random number generator will be used.
The sign bit will always be set to 0, thus all generated numbers will be positive.
Inputs:
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random 63 bit value
@@ -303,11 +182,7 @@ Example:
import "core:fmt"
int63_example :: proc() {
// Using the global random number generator
fmt.println(rand.int63())
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.int63(&my_rand))
}
Possible Output:
@@ -316,15 +191,12 @@ Possible Output:
389
*/
@(require_results) int63 :: proc(r: ^Rand = nil) -> (val: i64) { return i64(uint64(r) << 1 >> 1) }
@(require_results) int63 :: proc() -> (val: i64) { return i64(uint64() << 1 >> 1) }
/*
Generates a random 127 bit value using the provided random number generator. If no generator is provided the global random number generator will be used.
The sign bit will always be set to 0, thus all generated numbers will be positive.
Inputs:
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random 127 bit value
@@ -333,11 +205,7 @@ Example:
import "core:fmt"
int127_example :: proc() {
// Using the global random number generator
fmt.println(rand.int127())
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.int127(&my_rand))
}
Possible Output:
@@ -346,14 +214,13 @@ Possible Output:
389
*/
@(require_results) int127 :: proc(r: ^Rand = nil) -> (val: i128) { return i128(uint128(r) << 1 >> 1) }
@(require_results) int127 :: proc() -> (val: i128) { return i128(uint128() << 1 >> 1) }
/*
Generates a random 31 bit value in the range `[0, n)` using the provided random number generator. If no generator is provided the global random number generator will be used.
Inputs:
- n: The upper bound of the generated number, this value is exclusive
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random 31 bit value in the range `[0, n)`
@@ -365,11 +232,7 @@ Example:
import "core:fmt"
int31_max_example :: proc() {
// Using the global random number generator
fmt.println(rand.int31_max(16))
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.int31_max(1024, &my_rand))
}
Possible Output:
@@ -379,17 +242,17 @@ Possible Output:
*/
@(require_results)
int31_max :: proc(n: i32, r: ^Rand = nil) -> (val: i32) {
int31_max :: proc(n: i32) -> (val: i32) {
if n <= 0 {
panic("Invalid argument to int31_max")
}
if n&(n-1) == 0 {
return int31(r) & (n-1)
return int31() & (n-1)
}
max := i32((1<<31) - 1 - (1<<31)%u32(n))
v := int31(r)
v := int31()
for v > max {
v = int31(r)
v = int31()
}
return v % n
}
@@ -399,7 +262,6 @@ Generates a random 63 bit value in the range `[0, n)` using the provided random
Inputs:
- n: The upper bound of the generated number, this value is exclusive
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random 63 bit value in the range `[0, n)`
@@ -411,11 +273,7 @@ Example:
import "core:fmt"
int63_max_example :: proc() {
// Using the global random number generator
fmt.println(rand.int63_max(16))
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.int63_max(1024, &my_rand))
}
Possible Output:
@@ -425,17 +283,17 @@ Possible Output:
*/
@(require_results)
int63_max :: proc(n: i64, r: ^Rand = nil) -> (val: i64) {
int63_max :: proc(n: i64) -> (val: i64) {
if n <= 0 {
panic("Invalid argument to int63_max")
}
if n&(n-1) == 0 {
return int63(r) & (n-1)
return int63() & (n-1)
}
max := i64((1<<63) - 1 - (1<<63)%u64(n))
v := int63(r)
v := int63()
for v > max {
v = int63(r)
v = int63()
}
return v % n
}
@@ -445,7 +303,6 @@ Generates a random 127 bit value in the range `[0, n)` using the provided random
Inputs:
- n: The upper bound of the generated number, this value is exclusive
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random 127 bit value in the range `[0, n)`
@@ -457,11 +314,7 @@ Example:
import "core:fmt"
int127_max_example :: proc() {
// Using the global random number generator
fmt.println(rand.int127_max(16))
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.int127_max(1024, &my_rand))
}
Possible Output:
@@ -471,17 +324,17 @@ Possible Output:
*/
@(require_results)
int127_max :: proc(n: i128, r: ^Rand = nil) -> (val: i128) {
int127_max :: proc(n: i128) -> (val: i128) {
if n <= 0 {
panic("Invalid argument to int127_max")
}
if n&(n-1) == 0 {
return int127(r) & (n-1)
return int127() & (n-1)
}
max := i128((1<<127) - 1 - (1<<127)%u128(n))
v := int127(r)
v := int127()
for v > max {
v = int127(r)
v = int127()
}
return v % n
}
@@ -491,7 +344,6 @@ Generates a random integer value in the range `[0, n)` using the provided random
Inputs:
- n: The upper bound of the generated number, this value is exclusive
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random integer value in the range `[0, n)`
@@ -503,11 +355,7 @@ Example:
import "core:fmt"
int_max_example :: proc() {
// Using the global random number generator
fmt.println(rand.int_max(16))
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.int_max(1024, &my_rand))
}
Possible Output:
@@ -517,23 +365,20 @@ Possible Output:
*/
@(require_results)
int_max :: proc(n: int, r: ^Rand = nil) -> (val: int) {
int_max :: proc(n: int) -> (val: int) {
if n <= 0 {
panic("Invalid argument to int_max")
}
when size_of(int) == 4 {
return int(int31_max(i32(n), r))
return int(int31_max(i32(n)))
} else {
return int(int63_max(i64(n), r))
return int(int63_max(i64(n)))
}
}
/*
Generates a random double floating point value in the range `[0, 1)` using the provided random number generator. If no generator is provided the global random number generator will be used.
Inputs:
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random double floating point value in the range `[0, 1)`
@@ -542,11 +387,7 @@ Example:
import "core:fmt"
float64_example :: proc() {
// Using the global random number generator
fmt.println(rand.float64())
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.float64(&my_rand))
}
Possible Output:
@@ -555,14 +396,11 @@ Possible Output:
0.511
*/
@(require_results) float64 :: proc(r: ^Rand = nil) -> (val: f64) { return f64(int63_max(1<<53, r)) / (1 << 53) }
@(require_results) float64 :: proc() -> (val: f64) { return f64(int63_max(1<<53)) / (1 << 53) }
/*
Generates a random single floating point value in the range `[0, 1)` using the provided random number generator. If no generator is provided the global random number generator will be used.
Inputs:
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random single floating point value in the range `[0, 1)`
@@ -571,11 +409,7 @@ Example:
import "core:fmt"
float32_example :: proc() {
// Using the global random number generator
fmt.println(rand.float32())
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.float32(&my_rand))
}
Possible Output:
@@ -584,7 +418,7 @@ Possible Output:
0.511
*/
@(require_results) float32 :: proc(r: ^Rand = nil) -> (val: f32) { return f32(int31_max(1<<24, r)) / (1 << 24) }
@(require_results) float32 :: proc() -> (val: f32) { return f32(int31_max(1<<24)) / (1 << 24) }
/*
Generates a random double floating point value in the range `[low, high)` using the provided random number generator. If no generator is provided the global random number generator will be used.
@@ -594,7 +428,6 @@ WARNING: Panics if `high < low`
Inputs:
- low: The lower bounds of the value, this value is inclusive
- high: The upper bounds of the value, this value is exclusive
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random double floating point value in the range [low, high)
@@ -604,11 +437,7 @@ Example:
import "core:fmt"
float64_range_example :: proc() {
// Using the global random number generator
fmt.println(rand.float64_range(-10, 300))
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.float64_range(600, 900, &my_rand))
}
Possible Output:
@@ -617,9 +446,9 @@ Possible Output:
673.130
*/
@(require_results) float64_range :: proc(low, high: f64, r: ^Rand = nil) -> (val: f64) {
@(require_results) float64_range :: proc(low, high: f64) -> (val: f64) {
assert(low <= high, "low must be lower than or equal to high")
val = (high-low)*float64(r) + low
val = (high-low)*float64() + low
if val >= high {
val = max(low, high * (1 - math.F64_EPSILON))
}
@@ -632,7 +461,6 @@ Generates a random single floating point value in the range `[low, high)` using
Inputs:
- low: The lower bounds of the value, this value is inclusive
- high: The upper bounds of the value, this value is exclusive
- r: The random number generator to use, or nil for the global generator
Returns:
- val: A random single floating point value in the range [low, high)
@@ -644,11 +472,7 @@ Example:
import "core:fmt"
float32_range_example :: proc() {
// Using the global random number generator
fmt.println(rand.float32_range(-10, 300))
// Using local random number generator
my_rand := rand.create(1)
fmt.println(rand.float32_range(600, 900, &my_rand))
}
Possible Output:
@@ -657,9 +481,9 @@ Possible Output:
673.130
*/
@(require_results) float32_range :: proc(low, high: f32, r: ^Rand = nil) -> (val: f32) {
@(require_results) float32_range :: proc(low, high: f32) -> (val: f32) {
assert(low <= high, "low must be lower than or equal to high")
val = (high-low)*float32(r) + low
val = (high-low)*float32() + low
if val >= high {
val = max(low, high * (1 - math.F32_EPSILON))
}
@@ -672,7 +496,6 @@ Due to floating point precision there is no guarantee if the upper and lower bou
Inputs:
- p: The byte slice to fill
- r: The random number generator to use, or nil for the global generator
Returns:
- n: The number of bytes generated
@@ -682,7 +505,6 @@ Example:
import "core:fmt"
read_example :: proc() {
// Using the global random number generator
data: [8]byte
n := rand.read(data[:])
fmt.println(n)
@@ -696,12 +518,12 @@ Possible Output:
*/
@(require_results)
read :: proc(p: []byte, r: ^Rand = nil) -> (n: int) {
read :: proc(p: []byte) -> (n: int) {
pos := i8(0)
val := i64(0)
for n = 0; n < len(p); n += 1 {
if pos == 0 {
val = int63(r)
val = int63()
pos = 7
}
p[n] = byte(val)
@@ -718,7 +540,6 @@ Creates a slice of `int` filled with random values using the provided random num
Inputs:
- n: The size of the created slice
- r: The random number generator to use, or nil for the global generator
- allocator: (default: context.allocator)
Returns:
@@ -731,16 +552,10 @@ Example:
import "core:fmt"
perm_example :: proc() -> (err: mem.Allocator_Error) {
// Using the global random number generator and using the context allocator
data := rand.perm(4) or_return
fmt.println(data)
defer delete(data, context.allocator)
// Using local random number generator and temp allocator
my_rand := rand.create(1)
data_tmp := rand.perm(4, &my_rand, context.temp_allocator) or_return
fmt.println(data_tmp)
return
}
@@ -751,10 +566,10 @@ Possible Output:
*/
@(require_results)
perm :: proc(n: int, r: ^Rand = nil, allocator := context.allocator) -> (res: []int, err: mem.Allocator_Error) #optional_allocator_error {
perm :: proc(n: int, allocator := context.allocator) -> (res: []int, err: mem.Allocator_Error) #optional_allocator_error {
m := make([]int, n, allocator) or_return
for i := 0; i < n; i += 1 {
j := int_max(i+1, r)
j := int_max(i+1)
m[i] = m[j]
m[j] = i
}
@@ -766,14 +581,12 @@ Randomizes the ordering of elements for the provided slice. If no generator is p
Inputs:
- array: The slice to randomize
- r: The random number generator to use, or nil for the global generator
Example:
import "core:math/rand"
import "core:fmt"
shuffle_example :: proc() {
// Using the global random number generator
data: [4]int = { 1, 2, 3, 4 }
fmt.println(data) // the contents are in order
rand.shuffle(data[:])
@@ -786,14 +599,14 @@ Possible Output:
[2, 4, 3, 1]
*/
shuffle :: proc(array: $T/[]$E, r: ^Rand = nil) {
shuffle :: proc(array: $T/[]$E) {
n := i64(len(array))
if n < 2 {
return
}
for i := i64(n - 1); i > 0; i -= 1 {
j := int63_max(i + 1, r)
j := int63_max(i + 1)
array[i], array[j] = array[j], array[i]
}
}
@@ -803,7 +616,6 @@ Returns a random element from the provided slice. If no generator is provided th
Inputs:
- array: The slice to choose an element from
- r: The random number generator to use, or nil for the global generator
Returns:
- res: A random element from `array`
@@ -813,7 +625,6 @@ Example:
import "core:fmt"
choice_example :: proc() {
// Using the global random number generator
data: [4]int = { 1, 2, 3, 4 }
fmt.println(rand.choice(data[:]))
fmt.println(rand.choice(data[:]))
@@ -830,17 +641,17 @@ Possible Output:
*/
@(require_results)
choice :: proc(array: $T/[]$E, r: ^Rand = nil) -> (res: E) {
choice :: proc(array: $T/[]$E) -> (res: E) {
n := i64(len(array))
if n < 1 {
return E{}
}
return array[int63_max(n, r)]
return array[int63_max(n)]
}
@(require_results)
choice_enum :: proc($T: typeid, r: ^Rand = nil) -> T
choice_enum :: proc($T: typeid) -> T
where
intrinsics.type_is_enum(T),
size_of(T) <= 8,
@@ -848,11 +659,11 @@ choice_enum :: proc($T: typeid, r: ^Rand = nil) -> T
{
when intrinsics.type_is_unsigned(intrinsics.type_core_type(T)) &&
u64(max(T)) > u64(max(i64)) {
i := uint64(r) % u64(len(T))
i := uint64() % u64(len(T))
i += u64(min(T))
return T(i)
} else {
i := int63_max(i64(len(T)), r)
i := int63_max(i64(len(T)))
i += i64(min(T))
return T(i)
}

View File

@@ -180,7 +180,7 @@ binary_search_by :: proc(array: $A/[]$T, key: T, f: proc(T, T) -> Ordering) -> (
}
@(require_results)
equal :: proc(a, b: $T/[]$E) -> bool where intrinsics.type_is_comparable(E) {
equal :: proc(a, b: $T/[]$E) -> bool where intrinsics.type_is_comparable(E) #no_bounds_check {
if len(a) != len(b) {
return false
}
@@ -495,8 +495,10 @@ unique :: proc(s: $S/[]$T) -> S where intrinsics.type_is_comparable(T) #no_bound
}
i := 1
for j in 1..<len(s) {
if s[j] != s[j-1] && i != j {
s[i] = s[j]
if s[j] != s[j-1] {
if i != j {
s[i] = s[j]
}
i += 1
}
}
@@ -513,8 +515,10 @@ unique_proc :: proc(s: $S/[]$T, eq: proc(T, T) -> bool) -> S #no_bounds_check {
}
i := 1
for j in 1..<len(s) {
if !eq(s[j], s[j-1]) && i != j {
s[i] = s[j]
if !eq(s[j], s[j-1]) {
if i != j {
s[i] = s[j]
}
i += 1
}
}
@@ -736,4 +740,4 @@ bitset_to_enum_slice_with_make :: proc(bs: $T, $E: typeid, allocator := context.
return bitset_to_enum_slice(buf, bs)
}
bitset_to_enum_slice :: proc{bitset_to_enum_slice_with_make, bitset_to_enum_slice_with_buffer}
bitset_to_enum_slice :: proc{bitset_to_enum_slice_with_make, bitset_to_enum_slice_with_buffer}

View File

@@ -476,10 +476,7 @@ select_raw :: proc "odin" (recvs: []^Raw_Chan, sends: []^Raw_Chan, send_msgs: []
return
}
r: ^rand.Rand = nil
select_idx = rand.int_max(count, r) if count > 0 else 0
select_idx = rand.int_max(count) if count > 0 else 0
sel := candidates[select_idx]
if sel.is_recv {

View File

@@ -78,6 +78,7 @@ foreign kernel32 {
RemoveVectoredContinueHandler :: proc(Handle: LPVOID) -> DWORD ---
RaiseException :: proc(dwExceptionCode, dwExceptionFlags, nNumberOfArguments: DWORD, lpArguments: ^ULONG_PTR) -> ! ---
SetUnhandledExceptionFilter :: proc(lpTopLevelExceptionFilter: LPTOP_LEVEL_EXCEPTION_FILTER) -> LPTOP_LEVEL_EXCEPTION_FILTER ---
CreateHardLinkW :: proc(lpSymlinkFileName: LPCWSTR,
lpTargetFileName: LPCWSTR,
@@ -479,6 +480,8 @@ foreign kernel32 {
GetHandleInformation :: proc(hObject: HANDLE, lpdwFlags: ^DWORD) -> BOOL ---
RtlCaptureStackBackTrace :: proc(FramesToSkip: ULONG, FramesToCapture: ULONG, BackTrace: [^]PVOID, BackTraceHash: PULONG) -> USHORT ---
GetSystemPowerStatus :: proc(lpSystemPowerStatus: ^SYSTEM_POWER_STATUS) -> BOOL ---
}
DEBUG_PROCESS :: 0x00000001
@@ -1024,31 +1027,9 @@ foreign kernel32 {
HandlerRoutine :: proc "system" (dwCtrlType: DWORD) -> BOOL
PHANDLER_ROUTINE :: HandlerRoutine
// NOTE(Jeroen, 2024-06-13): As Odin now supports bit_fields, we no longer need
// a helper procedure. `init_dcb_with_config` and `get_dcb_config` have been removed.
DCB_Config :: struct {
fParity: bool,
fOutxCtsFlow: bool,
fOutxDsrFlow: bool,
fDtrControl: DTR_Control,
fDsrSensitivity: bool,
fTXContinueOnXoff: bool,
fOutX: bool,
fInX: bool,
fErrorChar: bool,
fNull: bool,
fRtsControl: RTS_Control,
fAbortOnError: bool,
BaudRate: DWORD,
ByteSize: BYTE,
Parity: Parity,
StopBits: Stop_Bits,
XonChar: byte,
XoffChar: byte,
ErrorChar: byte,
EvtChar: byte,
}
DTR_Control :: enum byte {
Disable = 0,
Enable = 1,
@@ -1073,92 +1054,35 @@ Stop_Bits :: enum byte {
Two = 2,
}
// A helper procedure to set the values of a DCB structure.
init_dcb_with_config :: proc "contextless" (dcb: ^DCB, config: DCB_Config) {
out: u32
// NOTE(tetra, 2022-09-21): On both Clang 14 on Windows, and MSVC, the bits in the bitfield
// appear to be defined from LSB to MSB order.
// i.e: `fBinary` (the first bitfield in the C source) is the LSB in the `settings` u32.
out |= u32(1) << 0 // fBinary must always be true on Windows.
out |= u32(config.fParity) << 1
out |= u32(config.fOutxCtsFlow) << 2
out |= u32(config.fOutxDsrFlow) << 3
out |= u32(config.fDtrControl) << 4
out |= u32(config.fDsrSensitivity) << 6
out |= u32(config.fTXContinueOnXoff) << 7
out |= u32(config.fOutX) << 8
out |= u32(config.fInX) << 9
out |= u32(config.fErrorChar) << 10
out |= u32(config.fNull) << 11
out |= u32(config.fRtsControl) << 12
out |= u32(config.fAbortOnError) << 14
dcb.settings = out
dcb.BaudRate = config.BaudRate
dcb.ByteSize = config.ByteSize
dcb.Parity = config.Parity
dcb.StopBits = config.StopBits
dcb.XonChar = config.XonChar
dcb.XoffChar = config.XoffChar
dcb.ErrorChar = config.ErrorChar
dcb.EvtChar = config.EvtChar
dcb.DCBlength = size_of(DCB)
}
get_dcb_config :: proc "contextless" (dcb: DCB) -> (config: DCB_Config) {
config.fParity = bool((dcb.settings >> 1) & 0x01)
config.fOutxCtsFlow = bool((dcb.settings >> 2) & 0x01)
config.fOutxDsrFlow = bool((dcb.settings >> 3) & 0x01)
config.fDtrControl = DTR_Control((dcb.settings >> 4) & 0x02)
config.fDsrSensitivity = bool((dcb.settings >> 6) & 0x01)
config.fTXContinueOnXoff = bool((dcb.settings >> 7) & 0x01)
config.fOutX = bool((dcb.settings >> 8) & 0x01)
config.fInX = bool((dcb.settings >> 9) & 0x01)
config.fErrorChar = bool((dcb.settings >> 10) & 0x01)
config.fNull = bool((dcb.settings >> 11) & 0x01)
config.fRtsControl = RTS_Control((dcb.settings >> 12) & 0x02)
config.fAbortOnError = bool((dcb.settings >> 14) & 0x01)
config.BaudRate = dcb.BaudRate
config.ByteSize = dcb.ByteSize
config.Parity = dcb.Parity
config.StopBits = dcb.StopBits
config.XonChar = dcb.XonChar
config.XoffChar = dcb.XoffChar
config.ErrorChar = dcb.ErrorChar
config.EvtChar = dcb.EvtChar
return
}
// NOTE(tetra): See get_dcb_config() and init_dcb_with_config() for help with initializing this.
DCB :: struct {
DCBlength: DWORD, // NOTE(tetra): Must be set to size_of(DCB).
BaudRate: DWORD,
settings: u32, // NOTE(tetra): These are bitfields in the C struct.
wReserved: WORD,
XOnLim: WORD,
XOffLim: WORD,
ByteSize: BYTE,
Parity: Parity,
StopBits: Stop_Bits,
XonChar: byte,
XoffChar: byte,
ErrorChar: byte,
EofChar: byte,
EvtChar: byte,
DCBlength: DWORD,
BaudRate: DWORD,
using _: bit_field DWORD {
fBinary: bool | 1,
fParity: bool | 1,
fOutxCtsFlow: bool | 1,
fOutxDsrFlow: bool | 1,
fDtrControl: DTR_Control | 2,
fDsrSensitivity: bool | 1,
fTXContinueOnXoff: bool | 1,
fOutX: bool | 1,
fInX: bool | 1,
fErrorChar: bool | 1,
fNull: bool | 1,
fRtsControl: RTS_Control | 2,
fAbortOnError: bool | 1,
},
wReserved: WORD,
XOnLim: WORD,
XOffLim: WORD,
ByteSize: BYTE,
Parity: Parity,
StopBits: Stop_Bits,
XonChar: byte,
XoffChar: byte,
ErrorChar: byte,
EofChar: byte,
EvtChar: byte,
wReserved1: WORD,
}
@@ -1238,6 +1162,30 @@ SYSTEM_LOGICAL_PROCESSOR_INFORMATION :: struct {
DummyUnion: DUMMYUNIONNAME_u,
}
SYSTEM_POWER_STATUS :: struct {
ACLineStatus: AC_Line_Status,
BatteryFlag: Battery_Flags,
BatteryLifePercent: BYTE,
SystemStatusFlag: BYTE,
BatteryLifeTime: DWORD,
BatteryFullLifeTime: DWORD,
}
AC_Line_Status :: enum BYTE {
Offline = 0,
Online = 1,
Unknown = 255,
}
Battery_Flag :: enum BYTE {
High = 0,
Low = 1,
Critical = 2,
Charging = 3,
No_Battery = 7,
}
Battery_Flags :: bit_set[Battery_Flag; BYTE]
/* Global Memory Flags */
GMEM_FIXED :: 0x0000
GMEM_MOVEABLE :: 0x0002
@@ -1256,3 +1204,5 @@ GMEM_INVALID_HANDLE :: 0x8000
GHND :: (GMEM_MOVEABLE | GMEM_ZEROINIT)
GPTR :: (GMEM_FIXED | GMEM_ZEROINIT)
LPTOP_LEVEL_EXCEPTION_FILTER :: PVECTORED_EXCEPTION_HANDLER

View File

@@ -34,6 +34,7 @@ HGDIOBJ :: distinct HANDLE
HBITMAP :: distinct HANDLE
HGLOBAL :: distinct HANDLE
HHOOK :: distinct HANDLE
HWINEVENTHOOK :: distinct HANDLE
HKEY :: distinct HANDLE
HDESK :: distinct HANDLE
HFONT :: distinct HANDLE
@@ -703,6 +704,14 @@ WNDPROC :: #type proc "system" (HWND, UINT, WPARAM, LPARAM) -> LRESULT
HOOKPROC :: #type proc "system" (code: c_int, wParam: WPARAM, lParam: LPARAM) -> LRESULT
WINEVENTPROC :: #type proc "system" (
hWinEventHook: HWINEVENTHOOK,
event: DWORD,
hwnd: HWND,
idObject, idChild: LONG,
idEventThread, dwmsEventTime: DWORD,
)
CWPRETSTRUCT :: struct {
lResult: LRESULT,
lParam: LPARAM,

View File

@@ -17,6 +17,18 @@ foreign user32 {
GetClassNameW :: proc(hWnd: HWND, lpClassName: LPWSTR, nMaxCount: c_int) -> c_int ---
GetParent :: proc(hWnd: HWND) -> HWND ---
IsWindowVisible :: proc(hWnd: HWND) -> BOOL ---
SetWinEventHook :: proc(
eventMin, eventMax: DWORD,
hmodWinEventProc: HMODULE,
pfnWinEvenProc: WINEVENTPROC,
idProcess, idThread: DWORD,
dwFlags: WinEventFlags,
) -> HWINEVENTHOOK ---
IsChild :: proc(hWndParent, hWnd: HWND) -> BOOL ---
RegisterClassW :: proc(lpWndClass: ^WNDCLASSW) -> ATOM ---
RegisterClassExW :: proc(^WNDCLASSEXW) -> ATOM ---
UnregisterClassW :: proc(lpClassName: LPCWSTR, hInstance: HINSTANCE) -> BOOL ---
@@ -568,3 +580,12 @@ RedrawWindowFlags :: enum UINT {
RDW_FRAME = 0x0400,
RDW_NOFRAME = 0x0800,
}
// OUTOFCONTEXT is the zero value, use {}
WinEventFlags :: bit_set[WinEventFlag; DWORD]
WinEventFlag :: enum DWORD {
SKIPOWNTHREAD = 0,
SKIPOWNPROCESS = 1,
INCONTEXT = 2,
}

View File

@@ -47,7 +47,7 @@ ERROR_PIPE_BUSY : DWORD : 231
E_NOTIMPL :: HRESULT(-0x7fff_bfff) // 0x8000_4001
SUCCEEDED :: #force_inline proc(#any_int result: int) -> bool { return result >= 0 }
SUCCEEDED :: #force_inline proc "contextless" (#any_int result: int) -> bool { return result >= 0 }
System_Error :: enum DWORD {

View File

@@ -8,13 +8,22 @@ import "core:strings"
import "core:sync/chan"
import "core:time"
Default_Test_Logger_Opts :: runtime.Logger_Options {
.Level,
.Terminal_Color,
.Short_File_Path,
.Line,
.Procedure,
.Date, .Time,
when USING_SHORT_LOGS {
Default_Test_Logger_Opts :: runtime.Logger_Options {
.Level,
.Terminal_Color,
.Short_File_Path,
.Line,
}
} else {
Default_Test_Logger_Opts :: runtime.Logger_Options {
.Level,
.Terminal_Color,
.Short_File_Path,
.Line,
.Procedure,
.Date, .Time,
}
}
Log_Message :: struct {

View File

@@ -9,6 +9,7 @@ import "core:encoding/ansi"
import "core:fmt"
import "core:io"
@require import pkg_log "core:log"
import "core:math/rand"
import "core:mem"
import "core:os"
import "core:slice"
@@ -41,6 +42,8 @@ PROGRESS_WIDTH : int : #config(ODIN_TEST_PROGRESS_WIDTH, 24)
SHARED_RANDOM_SEED : u64 : #config(ODIN_TEST_RANDOM_SEED, 0)
// Set the lowest log level for this test run.
LOG_LEVEL : string : #config(ODIN_TEST_LOG_LEVEL, "info")
// Show only the most necessary logging information.
USING_SHORT_LOGS : bool : #config(ODIN_TEST_SHORT_LOGS, false)
get_log_level :: #force_inline proc() -> runtime.Logger_Level {
@@ -104,6 +107,13 @@ run_test_task :: proc(task: thread.Task) {
options = Default_Test_Logger_Opts,
}
random_generator_state: runtime.Default_Random_State
context.random_generator = {
procedure = runtime.default_random_generator_proc,
data = &random_generator_state,
}
rand.reset(data.t.seed)
free_all(context.temp_allocator)
data.it.p(&data.t)
@@ -497,6 +507,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
data.it = it
data.t.seed = shared_random_seed
data.t.error_count = 0
data.t._fail_now_called = false
thread.pool_add_task(&pool, task.allocator, run_test_task, data, run_index)
}
@@ -604,10 +615,10 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
})
fmt.assertf(alloc_error == nil, "Error appending to log messages: %v", alloc_error)
find_task_data: for &data in task_data_slots {
find_task_data_for_timeout: for &data in task_data_slots {
if data.it.pkg == it.pkg && data.it.name == it.name {
end_t(&data.t)
break find_task_data
break find_task_data_for_timeout
}
}
}
@@ -645,21 +656,36 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
"A signal (%v) was raised to stop test #%i %s.%s, but it was unable to be found.",
reason, test_index, it.pkg, it.name)
if test_index not_in failed_test_reason_map {
// We only write a new error message here if there wasn't one
// already, because the message we can provide based only on
// the signal won't be very useful, whereas asserts and panics
// will provide a user-written error message.
failed_test_reason_map[test_index] = fmt.aprintf("Signal caught: %v", reason, allocator = shared_log_allocator)
pkg_log.fatalf("Caught signal to stop test #%i %s.%s for: %v.", test_index, it.pkg, it.name, reason)
// The order this is handled in is a little particular.
task_data: ^Task_Data
find_task_data_for_stop_signal: for &data in task_data_slots {
if data.it.pkg == it.pkg && data.it.name == it.name {
task_data = &data
break find_task_data_for_stop_signal
}
}
when FANCY_OUTPUT {
bypass_progress_overwrite = true
signals_were_raised = true
fmt.assertf(task_data != nil, "A signal (%v) was raised to stop test #%i %s.%s, but its task data is missing.",
reason, test_index, it.pkg, it.name)
if !task_data.t._fail_now_called {
if test_index not_in failed_test_reason_map {
// We only write a new error message here if there wasn't one
// already, because the message we can provide based only on
// the signal won't be very useful, whereas asserts and panics
// will provide a user-written error message.
failed_test_reason_map[test_index] = fmt.aprintf("Signal caught: %v", reason, allocator = shared_log_allocator)
pkg_log.fatalf("Caught signal to stop test #%i %s.%s for: %v.", test_index, it.pkg, it.name, reason)
}
when FANCY_OUTPUT {
bypass_progress_overwrite = true
signals_were_raised = true
}
}
end_t(&task_data.t)
total_failure_count += 1
total_done_count += 1
}

View File

@@ -48,7 +48,7 @@ T :: struct {
// tests during channel transmission.
_log_allocator: runtime.Allocator,
_fail_now: proc() -> !,
_fail_now_called: bool,
}
@@ -66,15 +66,20 @@ fail :: proc(t: ^T, loc := #caller_location) {
pkg_log.error("FAIL", location=loc)
}
fail_now :: proc(t: ^T, msg := "", loc := #caller_location) {
// fail_now will cause a test to immediately fail and abort, much in the same
// way a failed assertion or panic call will stop a thread.
//
// It is for when you absolutely need a test to fail without calling any of its
// deferred statements. It will be cleaner than a regular assert or panic,
// as the test runner will know to expect the signal this procedure will raise.
fail_now :: proc(t: ^T, msg := "", loc := #caller_location) -> ! {
t._fail_now_called = true
if msg != "" {
pkg_log.error("FAIL:", msg, location=loc)
} else {
pkg_log.error("FAIL", location=loc)
}
if t._fail_now != nil {
t._fail_now()
}
runtime.trap()
}
failed :: proc(t: ^T) -> bool {
@@ -94,7 +99,17 @@ logf :: proc(t: ^T, format: string, args: ..any, loc := #caller_location) {
// cleanup registers a procedure and user_data, which will be called when the test, and all its subtests, complete.
// Cleanup procedures will be called in LIFO (last added, first called) order.
// Each procedure will use a copy of the context at the time of registering.
//
// Each procedure will use a copy of the context at the time of registering,
// and if the test failed due to a timeout, failed assertion, panic, bounds-checking error,
// memory access violation, or any other signal-based fault, this procedure will
// run with greater privilege in the test runner's main thread.
//
// That means that any cleanup procedure absolutely must not fail in the same way,
// or it will take down the entire test runner with it. This is for when you
// need something to run no matter what, if a test failed.
//
// For almost every usual case, `defer` should be preferable and sufficient.
cleanup :: proc(t: ^T, procedure: proc(rawptr), user_data: rawptr) {
append(&t.cleanups, Internal_Cleanup{procedure, user_data, context})
}

View File

@@ -1,31 +1,44 @@
/*
The `i18n` package is flexible and easy to use.
The `i18n` package is a flexible and easy to use way to localise applications.
It has one call to get a translation: `get`, which the user can alias into something like `T`.
It has two calls to get a translation: `get()` and `get_n()`, which the user can alias into something like `T` and `Tn`
with statements like:
T :: i18n.get
Tn :: i18n.get_n.
`get`, referred to as `T` here, has a few different signatures.
All of them will return the key if the entry can't be found in the active translation catalog.
`get()` is used for retrieving the translation of sentences which **never** change in form,
like for instance "Connection established" or "All temporary files have been deleted".
Note that the number (singular, dual, plural, whatever else) is not relevant: the sentence is fixed and it will have only one possible translation in any other language.
- `T(key)` returns the translation of `key`.
- `T(key, n)` returns a pluralized translation of `key` according to value `n`.
`get_n()` is used for retrieving the translations of sentences which change according to the number of items referenced.
The various signatures of `get_n()` have one more parameter, `n`, which will receive that number and be used
to select the correct form according to the pluralizer attached to the message catalogue when initially loaded;
for instance, to summarise a rather complex matter, some languages use the singular form when referring to 0 items and some use the (only in their case) plural forms;
also, languages may have more or less quantifier forms than a single singular form and a universal plural form:
for instance, Chinese has just one form for any quantity, while Welsh may have up to 6 different forms for specific different quantities.
- `T(section, key)` returns the translation of `key` in `section`.
- `T(section, key, n)` returns a pluralized translation of `key` in `section` according to value `n`.
Both `get()` and `get_n()`, referred to as `T` and `Tn` here, have several different signatures.
All of them will return the key if the entry can't be found in the active translation catalogue.
By default lookup take place in the global `i18n.ACTIVE` catalogue for ease of use, unless a specific catalogue is supplied.
By default lookup take place in the global `i18n.ACTIVE` catalog for ease of use.
If you want to override which translation to use, for example in a language preview dialog, you can use the following:
- `T(key)` returns the translation of `key`.
- `T(key, catalog)` returns the translation of `key` from explictly supplied catalogue.
- `T(section, key)` returns the translation of `key` in `section`.
- `T(section, key, catalog)` returns the translation of `key` in `section` from explictly supplied catalogue.
- `T(key, n, catalog)` returns the pluralized version of `key` from explictly supplied catalog.
- `T(section, key, n, catalog)` returns the pluralized version of `key` in `section` from explictly supplied catalog.
- `Tn(key, n)` returns the translation of `key` according to number of items `n`.
- `Tn(key, n, catalog)` returns the translation of `key` from explictly supplied catalogue.
- `Tn(section, key, n)` returns the translation of `key` in `section` according to number of items `n`.
- `Tn(section, key, n, catalog)` returns the translation of `key` in `section` according to number of items `n` from explictly supplied catalogue.
If a catalog has translation contexts or sections, then omitting it in the above calls looks up in section "".
The default pluralization rule is n != 1, which is to say that passing n == 1 (or not passing n) returns the singular form.
Passing n != 1 returns plural form 1.
The default pluralization rule is `n != 1`, which is to say that passing `n == 1` returns the singular form (in slot 0).
Passing `n != 1` returns the plural form in slot 1 (if any).
Should a language not conform to this rule, you can pass a pluralizer procedure to the catalog parser.
This is a procedure that maps an integer to an integer, taking a value and returning which plural slot should be used.
This is a procedure that maps an integer to an integer, taking a quantity and returning which plural slot should be used.
You can also assign it to a loaded catalog after parsing, of course.
@@ -34,24 +47,21 @@ Example:
import "core:fmt"
import "core:text/i18n"
T :: i18n.get
T :: i18n.get
Tn :: i18n.get_n
mo :: proc() {
using fmt
err: i18n.Error
/*
Parse MO file and set it as the active translation so we can omit `get`'s "catalog" parameter.
*/
// Parse MO file and set it as the active translation so we can omit `get`'s "catalog" parameter.
i18n.ACTIVE, err = i18n.parse_mo(#load("translations/nl_NL.mo"))
defer i18n.destroy()
if err != .None { return }
/*
These are in the .MO catalog.
*/
// These are in the .MO catalog.
println("-----")
println(T(""))
println("-----")
@@ -60,13 +70,11 @@ Example:
println(T("Hellope, World!"))
println("-----")
// We pass 1 into `T` to get the singular format string, then 1 again into printf.
printf(T("There is %d leaf.\n", 1), 1)
printf(Tn("There is %d leaf.\n", 1), 1)
// We pass 42 into `T` to get the plural format string, then 42 again into printf.
printf(T("There is %d leaf.\n", 42), 42)
printf(Tn("There is %d leaf.\n", 42), 42)
/*
This isn't in the translation catalog, so the key is passed back untranslated.
*/
// This isn't in the translation catalog, so the key is passed back untranslated.
println("-----")
println(T("Come visit us on Discord!"))
}
@@ -76,19 +84,13 @@ Example:
err: i18n.Error
/*
Parse QT file and set it as the active translation so we can omit `get`'s "catalog" parameter.
*/
// Parse QT file and set it as the active translation so we can omit `get`'s "catalog" parameter.
i18n.ACTIVE, err = i18n.parse_qt(#load("translations/nl_NL-qt-ts.ts"))
defer i18n.destroy()
if err != .None {
return
}
if err != .None { return }
/*
These are in the .TS catalog. As you can see they have sections.
*/
// These are in the .TS catalog. As you can see they have sections.
println("--- Page section ---")
println("Page:Text for translation =", T("Page", "Text for translation"))
println("-----")
@@ -99,8 +101,8 @@ Example:
println("-----")
println("--- apple_count section ---")
println("apple_count:%d apple(s) =")
println("\t 1 =", T("apple_count", "%d apple(s)", 1))
println("\t 42 =", T("apple_count", "%d apple(s)", 42))
println("\t 1 =", Tn("apple_count", "%d apple(s)", 1))
println("\t 42 =", Tn("apple_count", "%d apple(s)", 42))
}
*/
package i18n

View File

@@ -10,23 +10,13 @@ package i18n
*/
import "core:strings"
/*
TODO:
- Support for more translation catalog file formats.
*/
/*
Currently active catalog.
*/
// Currently active catalog.
ACTIVE: ^Translation
// Allow between 1 and 255 plural forms. Default: 10.
MAX_PLURALS :: min(max(#config(ODIN_i18N_MAX_PLURAL_FORMS, 10), 1), 255)
/*
The main data structure. This can be generated from various different file formats, as long as we have a parser for them.
*/
// The main data structure. This can be generated from various different file formats, as long as we have a parser for them.
Section :: map[string][]string
Translation :: struct {
@@ -37,34 +27,24 @@ Translation :: struct {
}
Error :: enum {
/*
General return values.
*/
// General return values.
None = 0,
Empty_Translation_Catalog,
Duplicate_Key,
/*
Couldn't find, open or read file.
*/
// Couldn't find, open or read file.
File_Error,
/*
File too short.
*/
// File too short.
Premature_EOF,
/*
GNU Gettext *.MO file errors.
*/
// GNU Gettext *.MO file errors.
MO_File_Invalid_Signature,
MO_File_Unsupported_Version,
MO_File_Invalid,
MO_File_Incorrect_Plural_Count,
/*
Qt Linguist *.TS file errors.
*/
// Qt Linguist *.TS file errors.
TS_File_Parse_Error,
TS_File_Expected_Context,
TS_File_Expected_Context_Name,
@@ -85,73 +65,142 @@ DEFAULT_PARSE_OPTIONS :: Parse_Options{
}
/*
Several ways to use:
- get(key), which defaults to the singular form and i18n.ACTIVE catalog, or
- get(key, number), which returns the appropriate plural from the active catalog, or
- get(key, number, catalog) to grab text from a specific one.
*/
get_single_section :: proc(key: string, number := 1, catalog: ^Translation = ACTIVE) -> (value: string) {
/*
A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
*/
plural := 1 if number != 1 else 0
Returns the first translation string for the passed `key`.
It is also aliased with `get()`.
if catalog.pluralize != nil {
plural = catalog.pluralize(number)
}
return get_by_slot(key, plural, catalog)
Two ways to use it:
- get(key), which defaults to the `i18n.ACTIVE` catalogue, or
- get(key, catalog) to grab text from a specific loaded catalogue
Inputs:
- key: the string to translate
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
Returns: the translated string, or the original `key` if no translation was found.
*/
get_single_section :: proc(key: string, catalog: ^Translation = ACTIVE) -> (value: string) {
return get_by_slot(key, 0, catalog)
}
/*
Several ways to use:
- get(section, key), which defaults to the singular form and i18n.ACTIVE catalog, or
- get(section, key, number), which returns the appropriate plural from the active catalog, or
- get(section, key, number, catalog) to grab text from a specific one.
*/
get_by_section :: proc(section, key: string, number := 1, catalog: ^Translation = ACTIVE) -> (value: string) {
/*
A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
*/
plural := 1 if number != 1 else 0
Returns the first translation string for the passed `key` in a specific section or context.
It is also aliases with `get()`.
if catalog.pluralize != nil {
plural = catalog.pluralize(number)
}
return get_by_slot(section, key, plural, catalog)
Two ways to use it:
- get(section, key), which defaults to the `i18n.ACTIVE` catalogue, or
- get(section, key, catalog) to grab text from a specific loaded catalogue
Inputs:
- section: the catalogue section (sometimes also called 'context') in which to look up the translation
- key: the string to translate
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
Returns: the translated string, or the original `key` if no translation was found.
*/
get_by_section :: proc(section, key: string, catalog: ^Translation = ACTIVE) -> (value: string) {
return get_by_slot(section, key, 0, catalog)
}
get :: proc{get_single_section, get_by_section}
/*
Several ways to use:
- get_by_slot(key), which defaults to the singular form and i18n.ACTIVE catalog, or
- get_by_slot(key, slot), which returns the requested plural from the active catalog, or
- get_by_slot(key, slot, catalog) to grab text from a specific one.
Returns the translation string for the passed `key` in a specific plural form (if present in the catalogue).
It is also aliased with `get_n()`.
Two ways to use it:
- get_n(key, quantity), which returns the appropriate plural from the active catalogue, or
- get_n(key, quantity, catalog) to grab text from a specific loaded catalogue
Inputs:
- key: the string to translate
- quantity: the quantity of item to be used to select the correct plural form
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
Returns: the translated string, or the original `key` if no translation was found.
*/
get_single_section_with_quantity :: proc(key: string, quantity: int, catalog: ^Translation = ACTIVE) -> (value: string) {
/*
A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
*/
slot := 1 if quantity != 1 else 0
if catalog.pluralize != nil {
slot = catalog.pluralize(quantity)
}
return get_by_slot(key, slot, catalog)
}
/*
Returns the translation string for the passed `key` in a specific plural form (if present in the catalogue)
in a specific section or context.
It is also aliases with `get_n()`.
Two ways to use it:
- get(section, key, quantity), which returns the appropriate plural from the active catalogue, or
- get(section, key, quantity, catalog) to grab text from a specific loaded catalogue
Inputs:
- section: the catalogue section (sometime also called 'context') from which to lookup the translation
- key: the string to translate
- qantity: the quantity of item to be used to select the correct plural form
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
Returns: the translated string, or the original `key` if no translation was found
*/
get_by_section_with_quantity :: proc(section, key: string, quantity: int, catalog: ^Translation = ACTIVE) -> (value: string) {
/*
A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
*/
slot := 1 if quantity != 1 else 0
if catalog.pluralize != nil {
slot = catalog.pluralize(quantity)
}
return get_by_slot(section, key, slot, catalog)
}
get_n :: proc{get_single_section_with_quantity, get_by_section_with_quantity}
/*
Two ways to use:
- get_by_slot(key, slot), which returns the requested plural from the active catalogue, or
- get_by_slot(key, slot, catalog) to grab text from a specific loaded catalogue.
If a file format parser doesn't (yet) support plural slots, each of the slots will point at the same string.
- section: the catalogue section (sometime also called 'context') from which to lookup the translation
Inputs:
- key: the string to translate.
- slot: the translation slot to choose (slots refer to plural forms specific for each language and their meaning changes from catalogue to catalogue).
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
Returns: the translated string, or the original `key` if no translation was found.
*/
get_by_slot_single_section :: proc(key: string, slot := 0, catalog: ^Translation = ACTIVE) -> (value: string) {
get_by_slot_single_section :: proc(key: string, slot: int, catalog: ^Translation = ACTIVE) -> (value: string) {
return get_by_slot_by_section("", key, slot, catalog)
}
/*
Several ways to use:
- get_by_slot(key), which defaults to the singular form and i18n.ACTIVE catalog, or
Two ways to use:
- get_by_slot(key, slot), which returns the requested plural from the active catalog, or
- get_by_slot(key, slot, catalog) to grab text from a specific one.
If a file format parser doesn't (yet) support plural slots, each of the slots will point at the same string.
Inputs:
- section: the catalogue section (sometime also called 'context') from which to lookup the translation
- key: the string to translate.
- slot: the translation slot to choose (slots refer to plural forms specific for each language and their meaning changes from catalogue to catalogue).
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
Returns: the translated string or the original `key` if no translation was found.
*/
get_by_slot_by_section :: proc(section, key: string, slot := 0, catalog: ^Translation = ACTIVE) -> (value: string) {
get_by_slot_by_section :: proc(section, key: string, slot: int, catalog: ^Translation = ACTIVE) -> (value: string) {
if catalog == nil || section not_in catalog.k_v {
/*
Return the key if the catalog catalog hasn't been initialized yet, or the section is not present.
*/
// Return the key if the catalog catalog hasn't been initialized yet, or the section is not present.
return key
}
/*
Return the translation from the requested slot if this key is known, else return the key.
*/
// Return the translation from the requested slot if this key is known, else return the key.
if translations, ok := catalog.k_v[section][key]; ok {
plural := min(max(0, slot), len(catalog.k_v[section][key]) - 1)
return translations[plural]
@@ -161,7 +210,6 @@ get_by_slot_by_section :: proc(section, key: string, slot := 0, catalog: ^Transl
get_by_slot :: proc{get_by_slot_single_section, get_by_slot_by_section}
/*
Same for destroy:
- destroy(), to clean up the currently active catalog catalog i18n.ACTIVE
- destroy(catalog), to clean up a specific catalog.
*/

View File

@@ -5,6 +5,10 @@ REPLACEMENT_CHAR :: '\ufffd' // Represented an invalid code point
MAX_ASCII :: '\u007f' // Maximum ASCII value
MAX_LATIN1 :: '\u00ff' // Maximum Latin-1 value
ZERO_WIDTH_NON_JOINER :: '\u200C'
ZERO_WIDTH_JOINER :: '\u200D'
@(require_results)
binary_search :: proc(c: i32, table: []i32, length, stride: int) -> int {
n := length
t := 0
@@ -24,6 +28,7 @@ binary_search :: proc(c: i32, table: []i32, length, stride: int) -> int {
return -1
}
@(require_results)
to_lower :: proc(r: rune) -> rune {
c := i32(r)
p := binary_search(c, to_lower_ranges[:], len(to_lower_ranges)/3, 3)
@@ -36,6 +41,7 @@ to_lower :: proc(r: rune) -> rune {
}
return rune(c)
}
@(require_results)
to_upper :: proc(r: rune) -> rune {
c := i32(r)
p := binary_search(c, to_upper_ranges[:], len(to_upper_ranges)/3, 3)
@@ -48,6 +54,7 @@ to_upper :: proc(r: rune) -> rune {
}
return rune(c)
}
@(require_results)
to_title :: proc(r: rune) -> rune {
c := i32(r)
p := binary_search(c, to_upper_singlets[:], len(to_title_singlets)/2, 2)
@@ -58,6 +65,7 @@ to_title :: proc(r: rune) -> rune {
}
@(require_results)
is_lower :: proc(r: rune) -> bool {
if r <= MAX_ASCII {
return u32(r)-'a' < 26
@@ -74,6 +82,7 @@ is_lower :: proc(r: rune) -> bool {
return false
}
@(require_results)
is_upper :: proc(r: rune) -> bool {
if r <= MAX_ASCII {
return u32(r)-'A' < 26
@@ -91,6 +100,7 @@ is_upper :: proc(r: rune) -> bool {
}
is_alpha :: is_letter
@(require_results)
is_letter :: proc(r: rune) -> bool {
if u32(r) <= MAX_LATIN1 {
return char_properties[u8(r)]&pLmask != 0
@@ -111,10 +121,12 @@ is_letter :: proc(r: rune) -> bool {
return false
}
@(require_results)
is_title :: proc(r: rune) -> bool {
return is_upper(r) && is_lower(r)
}
@(require_results)
is_digit :: proc(r: rune) -> bool {
if r <= MAX_LATIN1 {
return '0' <= r && r <= '9'
@@ -124,6 +136,7 @@ is_digit :: proc(r: rune) -> bool {
is_white_space :: is_space
@(require_results)
is_space :: proc(r: rune) -> bool {
if u32(r) <= MAX_LATIN1 {
switch r {
@@ -140,18 +153,20 @@ is_space :: proc(r: rune) -> bool {
return false
}
@(require_results)
is_combining :: proc(r: rune) -> bool {
c := i32(r)
return c >= 0x0300 && (c <= 0x036f ||
(c >= 0x1ab0 && c <= 0x1aff) ||
(c >= 0x1dc0 && c <= 0x1dff) ||
(c >= 0x20d0 && c <= 0x20ff) ||
(c >= 0xfe20 && c <= 0xfe2f))
(c >= 0x1ab0 && c <= 0x1aff) ||
(c >= 0x1dc0 && c <= 0x1dff) ||
(c >= 0x20d0 && c <= 0x20ff) ||
(c >= 0xfe20 && c <= 0xfe2f))
}
@(require_results)
is_graphic :: proc(r: rune) -> bool {
if u32(r) <= MAX_LATIN1 {
return char_properties[u8(r)]&pg != 0
@@ -159,6 +174,7 @@ is_graphic :: proc(r: rune) -> bool {
return false
}
@(require_results)
is_print :: proc(r: rune) -> bool {
if u32(r) <= MAX_LATIN1 {
return char_properties[u8(r)]&pp != 0
@@ -166,6 +182,7 @@ is_print :: proc(r: rune) -> bool {
return false
}
@(require_results)
is_control :: proc(r: rune) -> bool {
if u32(r) <= MAX_LATIN1 {
return char_properties[u8(r)]&pC != 0
@@ -173,6 +190,7 @@ is_control :: proc(r: rune) -> bool {
return false
}
@(require_results)
is_number :: proc(r: rune) -> bool {
if u32(r) <= MAX_LATIN1 {
return char_properties[u8(r)]&pN != 0
@@ -180,6 +198,7 @@ is_number :: proc(r: rune) -> bool {
return false
}
@(require_results)
is_punct :: proc(r: rune) -> bool {
if u32(r) <= MAX_LATIN1 {
return char_properties[u8(r)]&pP != 0
@@ -187,9 +206,250 @@ is_punct :: proc(r: rune) -> bool {
return false
}
@(require_results)
is_symbol :: proc(r: rune) -> bool {
if u32(r) <= MAX_LATIN1 {
return char_properties[u8(r)]&pS != 0
}
return false
}
//
// The procedures below are accurate as of Unicode 15.1.0.
//
// Emoji_Modifier
@(require_results)
is_emoji_modifier :: proc(r: rune) -> bool {
return 0x1F3FB <= r && r <= 0x1F3FF
}
// Regional_Indicator
@(require_results)
is_regional_indicator :: proc(r: rune) -> bool {
return 0x1F1E6 <= r && r <= 0x1F1FF
}
// General_Category=Enclosing_Mark
@(require_results)
is_enclosing_mark :: proc(r: rune) -> bool {
switch r {
case 0x0488,
0x0489,
0x1ABE,
0x20DD ..= 0x20E0,
0x20E2 ..= 0x20E4,
0xA670 ..= 0xA672:
return true
}
return false
}
// Prepended_Concatenation_Mark
@(require_results)
is_prepended_concatenation_mark :: proc(r: rune) -> bool {
switch r {
case 0x00600 ..= 0x00605,
0x006DD,
0x0070F,
0x00890 ..= 0x00891,
0x008E2,
0x110BD,
0x110CD:
return true
case:
return false
}
}
// General_Category=Spacing_Mark
@(require_results)
is_spacing_mark :: proc(r: rune) -> bool {
c := i32(r)
p := binary_search(c, spacing_mark_ranges[:], len(spacing_mark_ranges)/2, 2)
if p >= 0 && spacing_mark_ranges[p] <= c && c <= spacing_mark_ranges[p+1] {
return true
}
return false
}
// General_Category=Nonspacing_Mark
@(require_results)
is_nonspacing_mark :: proc(r: rune) -> bool {
c := i32(r)
p := binary_search(c, nonspacing_mark_ranges[:], len(nonspacing_mark_ranges)/2, 2)
if p >= 0 && nonspacing_mark_ranges[p] <= c && c <= nonspacing_mark_ranges[p+1] {
return true
}
return false
}
// Extended_Pictographic
@(require_results)
is_emoji_extended_pictographic :: proc(r: rune) -> bool {
c := i32(r)
p := binary_search(c, emoji_extended_pictographic_ranges[:], len(emoji_extended_pictographic_ranges)/2, 2)
if p >= 0 && emoji_extended_pictographic_ranges[p] <= c && c <= emoji_extended_pictographic_ranges[p+1] {
return true
}
return false
}
// Grapheme_Extend
@(require_results)
is_grapheme_extend :: proc(r: rune) -> bool {
c := i32(r)
p := binary_search(c, grapheme_extend_ranges[:], len(grapheme_extend_ranges)/2, 2)
if p >= 0 && grapheme_extend_ranges[p] <= c && c <= grapheme_extend_ranges[p+1] {
return true
}
return false
}
// Hangul_Syllable_Type=Leading_Jamo
@(require_results)
is_hangul_syllable_leading :: proc(r: rune) -> bool {
return 0x1100 <= r && r <= 0x115F || 0xA960 <= r && r <= 0xA97C
}
// Hangul_Syllable_Type=Vowel_Jamo
@(require_results)
is_hangul_syllable_vowel :: proc(r: rune) -> bool {
return 0x1160 <= r && r <= 0x11A7 || 0xD7B0 <= r && r <= 0xD7C6
}
// Hangul_Syllable_Type=Trailing_Jamo
@(require_results)
is_hangul_syllable_trailing :: proc(r: rune) -> bool {
return 0x11A8 <= r && r <= 0x11FF || 0xD7CB <= r && r <= 0xD7FB
}
// Hangul_Syllable_Type=LV_Syllable
@(require_results)
is_hangul_syllable_lv :: proc(r: rune) -> bool {
c := i32(r)
p := binary_search(c, hangul_syllable_lv_singlets[:], len(hangul_syllable_lv_singlets), 1)
if p >= 0 && c == hangul_syllable_lv_singlets[p] {
return true
}
return false
}
// Hangul_Syllable_Type=LVT_Syllable
@(require_results)
is_hangul_syllable_lvt :: proc(r: rune) -> bool {
c := i32(r)
p := binary_search(c, hangul_syllable_lvt_ranges[:], len(hangul_syllable_lvt_ranges)/2, 2)
if p >= 0 && hangul_syllable_lvt_ranges[p] <= c && c <= hangul_syllable_lvt_ranges[p+1] {
return true
}
return false
}
// Indic_Syllabic_Category=Consonant_Preceding_Repha
@(require_results)
is_indic_consonant_preceding_repha :: proc(r: rune) -> bool {
switch r {
case 0x00D4E,
0x11941,
0x11D46,
0x11F02:
return true
case:
return false
}
}
// Indic_Syllabic_Category=Consonant_Prefixed
@(require_results)
is_indic_consonant_prefixed :: proc(r: rune) -> bool {
switch r {
case 0x111C2 ..= 0x111C3,
0x1193F,
0x11A3A,
0x11A84 ..= 0x11A89:
return true
case:
return false
}
}
// Indic_Conjunct_Break=Linker
@(require_results)
is_indic_conjunct_break_linker :: proc(r: rune) -> bool {
switch r {
case 0x094D,
0x09CD,
0x0ACD,
0x0B4D,
0x0C4D,
0x0D4D:
return true
case:
return false
}
}
// Indic_Conjunct_Break=Consonant
@(require_results)
is_indic_conjunct_break_consonant :: proc(r: rune) -> bool {
c := i32(r)
p := binary_search(c, indic_conjunct_break_consonant_ranges[:], len(indic_conjunct_break_consonant_ranges)/2, 2)
if p >= 0 && indic_conjunct_break_consonant_ranges[p] <= c && c <= indic_conjunct_break_consonant_ranges[p+1] {
return true
}
return false
}
// Indic_Conjunct_Break=Extend
@(require_results)
is_indic_conjunct_break_extend :: proc(r: rune) -> bool {
c := i32(r)
p := binary_search(c, indic_conjunct_break_extend_ranges[:], len(indic_conjunct_break_extend_ranges)/2, 2)
if p >= 0 && indic_conjunct_break_extend_ranges[p] <= c && c <= indic_conjunct_break_extend_ranges[p+1] {
return true
}
return false
}
/*
For grapheme text segmentation, from Unicode TR 29 Rev 43:
```
Indic_Syllabic_Category = Consonant_Preceding_Repha, or
Indic_Syllabic_Category = Consonant_Prefixed, or
Prepended_Concatenation_Mark = Yes
```
*/
@(require_results)
is_gcb_prepend_class :: proc(r: rune) -> bool {
return is_indic_consonant_preceding_repha(r) || is_indic_consonant_prefixed(r) || is_prepended_concatenation_mark(r)
}
/*
For grapheme text segmentation, from Unicode TR 29 Rev 43:
```
Grapheme_Extend = Yes, or
Emoji_Modifier = Yes
This includes:
General_Category = Nonspacing_Mark
General_Category = Enclosing_Mark
U+200C ZERO WIDTH NON-JOINER
plus a few General_Category = Spacing_Mark needed for canonical equivalence.
```
*/
@(require_results)
is_gcb_extend_class :: proc(r: rune) -> bool {
return is_grapheme_extend(r) || is_emoji_modifier(r)
}
//
// End of Unicode 15.1.0 block.
//

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,387 @@
package utf8
import "core:unicode"
ZERO_WIDTH_JOINER :: unicode.ZERO_WIDTH_JOINER
is_control :: unicode.is_control
is_hangul_syllable_leading :: unicode.is_hangul_syllable_leading
is_hangul_syllable_vowel :: unicode.is_hangul_syllable_vowel
is_hangul_syllable_trailing :: unicode.is_hangul_syllable_trailing
is_hangul_syllable_lv :: unicode.is_hangul_syllable_lv
is_hangul_syllable_lvt :: unicode.is_hangul_syllable_lvt
is_indic_conjunct_break_extend :: unicode.is_indic_conjunct_break_extend
is_indic_conjunct_break_linker :: unicode.is_indic_conjunct_break_linker
is_indic_conjunct_break_consonant :: unicode.is_indic_conjunct_break_consonant
is_gcb_extend_class :: unicode.is_gcb_extend_class
is_spacing_mark :: unicode.is_spacing_mark
is_gcb_prepend_class :: unicode.is_gcb_prepend_class
is_emoji_extended_pictographic :: unicode.is_emoji_extended_pictographic
is_regional_indicator :: unicode.is_regional_indicator
Grapheme :: struct {
byte_index: int,
rune_index: int,
}
/*
Count the individual graphemes in a UTF-8 string.
Inputs:
- str: The input string.
Returns:
- graphemes: The number of graphemes in the string.
- runes: The number of runes in the string.
*/
@(require_results)
grapheme_count :: proc(str: string) -> (graphemes, runes: int) {
_, graphemes, runes = decode_grapheme_clusters(str, false)
return
}
/*
Decode the individual graphemes in a UTF-8 string.
*Allocates Using Provided Allocator*
Inputs:
- str: The input string.
- track_graphemes: Whether or not to allocate and return `graphemes` with extra data about each grapheme.
- allocator: (default: context.allocator)
Returns:
- graphemes: Extra data about each grapheme.
- grapheme_count: The number of graphemes in the string.
- rune_count: The number of runes in the string.
*/
@(require_results)
decode_grapheme_clusters :: proc(
str: string,
track_graphemes := true,
allocator := context.allocator,
) -> (
graphemes: [dynamic]Grapheme,
grapheme_count: int,
rune_count: int,
) {
// The following procedure implements text segmentation by breaking on
// Grapheme Cluster Boundaries[1], using the values[2] and rules[3] from
// the Unicode® Standard Annex #29, entitled:
//
// UNICODE TEXT SEGMENTATION
//
// Version: Unicode 15.1.0
// Date: 2023-08-16
// Revision: 43
//
// This procedure is conformant[4] to UAX29-C1-1, otherwise known as the
// extended, non-legacy ruleset.
//
// Please see the references below for more information.
//
//
// NOTE(Feoramund): This procedure has not been highly optimized.
// A couple opportunities were taken to bypass repeated checking when a
// rune is outside of certain codepoint ranges, but little else has been
// done. Standard switches, conditionals, and binary search are used to
// see if a rune fits into a certain category.
//
// I did find that only one prior rune of state was necessary to build an
// algorithm that successfully passes all 4,835 test cases provided with
// this implementation from the Unicode organization's website.
//
// My initial implementation tracked explicit breaks and counted them once
// the string iteration had terminated. I've found this current
// implementation to be far simpler and need no allocations (unless the
// caller wants position data).
//
// Most rules work backwards instead of forwards which has helped keep this
// simple, despite its length and verbosity.
//
//
// The implementation has been left verbose and in the order described by
// the specification, to enable better readability and future upkeep.
//
// Some possible optimizations might include:
//
// - saving the type of `last_rune` instead of the exact rune.
// - reordering rules.
// - combining tables.
//
//
// [1]: https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
// [2]: https://www.unicode.org/reports/tr29/#Default_Grapheme_Cluster_Table
// [3]: https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules
// [4]: https://www.unicode.org/reports/tr29/#Conformance
Grapheme_Cluster_Sequence :: enum {
None,
Indic,
Emoji,
Regional,
}
context.allocator = allocator
last_rune: rune
last_rune_breaks_forward: bool
last_grapheme_count: int
bypass_next_rune: bool
regional_indicator_counter: int
current_sequence: Grapheme_Cluster_Sequence
continue_sequence: bool
for this_rune, byte_index in str {
defer {
// "Break at the start and end of text, unless the text is empty."
//
// GB1: sot ÷ Any
// GB2: Any ÷ eot
if rune_count == 0 && grapheme_count == 0 {
grapheme_count += 1
}
if track_graphemes && grapheme_count > last_grapheme_count {
append(&graphemes, Grapheme{ byte_index, rune_count })
}
last_grapheme_count = grapheme_count
last_rune = this_rune
rune_count += 1
if !continue_sequence {
current_sequence = .None
regional_indicator_counter = 0
}
continue_sequence = false
}
// "Do not break between a CR and LF. Otherwise, break before and after controls."
//
// GB3: CR × LF
// GB4: (Control | CR | LF) ÷
// GB5: ÷ (Control | CR | LF)
if this_rune == '\n' && last_rune == '\r' {
last_rune_breaks_forward = false
bypass_next_rune = false
continue
}
if is_control(this_rune) {
grapheme_count += 1
last_rune_breaks_forward = true
bypass_next_rune = true
continue
}
// (This check is for rules that work forwards, instead of backwards.)
if bypass_next_rune {
if last_rune_breaks_forward {
grapheme_count += 1
last_rune_breaks_forward = false
}
bypass_next_rune = false
continue
}
// (Optimization 1: Prevent low runes from proceeding further.)
//
// * 0xA9 and 0xAE are in the Extended_Pictographic range,
// which is checked later in GB11.
if this_rune != 0xA9 && this_rune != 0xAE && this_rune <= 0x2FF {
grapheme_count += 1
continue
}
// (Optimization 2: Check if the rune is in the Hangul space before getting specific.)
if 0x1100 <= this_rune && this_rune <= 0xD7FB {
// "Do not break Hangul syllable sequences."
//
// GB6: L × (L | V | LV | LVT)
// GB7: (LV | V) × (V | T)
// GB8: (LVT | T) × T
if is_hangul_syllable_leading(this_rune) ||
is_hangul_syllable_lv(this_rune) ||
is_hangul_syllable_lvt(this_rune)
{
if !is_hangul_syllable_leading(last_rune) {
grapheme_count += 1
}
continue
}
if is_hangul_syllable_vowel(this_rune) {
if is_hangul_syllable_leading(last_rune) ||
is_hangul_syllable_vowel(last_rune) ||
is_hangul_syllable_lv(last_rune)
{
continue
}
grapheme_count += 1
continue
}
if is_hangul_syllable_trailing(this_rune) {
if is_hangul_syllable_trailing(last_rune) ||
is_hangul_syllable_lvt(last_rune) ||
is_hangul_syllable_lv(last_rune) ||
is_hangul_syllable_vowel(last_rune)
{
continue
}
grapheme_count += 1
continue
}
}
// "Do not break before extending characters or ZWJ."
//
// GB9: × (Extend | ZWJ)
if this_rune == ZERO_WIDTH_JOINER {
continue_sequence = true
continue
}
if is_gcb_extend_class(this_rune) {
// (Support for GB9c.)
if current_sequence == .Indic {
if is_indic_conjunct_break_extend(this_rune) && (
is_indic_conjunct_break_linker(last_rune) ||
is_indic_conjunct_break_consonant(last_rune) )
{
continue_sequence = true
continue
}
if is_indic_conjunct_break_linker(this_rune) && (
is_indic_conjunct_break_linker(last_rune) ||
is_indic_conjunct_break_extend(last_rune) ||
is_indic_conjunct_break_consonant(last_rune) )
{
continue_sequence = true
continue
}
continue
}
// (Support for GB11.)
if current_sequence == .Emoji && (
is_gcb_extend_class(last_rune) ||
is_emoji_extended_pictographic(last_rune) )
{
continue_sequence = true
}
continue
}
// _The GB9a and GB9b rules only apply to extended grapheme clusters:_
// "Do not break before SpacingMarks, or after Prepend characters."
//
// GB9a: × SpacingMark
// GB9b: Prepend ×
if is_spacing_mark(this_rune) {
continue
}
if is_gcb_prepend_class(this_rune) {
grapheme_count += 1
bypass_next_rune = true
continue
}
// _The GB9c rule only applies to extended grapheme clusters:_
// "Do not break within certain combinations with Indic_Conjunct_Break (InCB)=Linker."
//
// GB9c: \p{InCB=Consonant} [ \p{InCB=Extend} \p{InCB=Linker} ]* \p{InCB=Linker} [ \p{InCB=Extend} \p{InCB=Linker} ]* × \p{InCB=Consonant}
if is_indic_conjunct_break_consonant(this_rune) {
if current_sequence == .Indic {
if last_rune == ZERO_WIDTH_JOINER ||
is_indic_conjunct_break_linker(last_rune)
{
continue_sequence = true
} else {
grapheme_count += 1
}
} else {
grapheme_count += 1
current_sequence = .Indic
continue_sequence = true
}
continue
}
if is_indic_conjunct_break_extend(this_rune) {
if current_sequence == .Indic {
if is_indic_conjunct_break_consonant(last_rune) ||
is_indic_conjunct_break_linker(last_rune)
{
continue_sequence = true
} else {
grapheme_count += 1
}
}
continue
}
if is_indic_conjunct_break_linker(this_rune) {
if current_sequence == .Indic {
if is_indic_conjunct_break_extend(last_rune) ||
is_indic_conjunct_break_linker(last_rune)
{
continue_sequence = true
} else {
grapheme_count += 1
}
}
continue
}
//
// (Curiously, there is no GB10.)
//
// "Do not break within emoji modifier sequences or emoji zwj sequences."
//
// GB11: \p{Extended_Pictographic} Extend* ZWJ × \p{Extended_Pictographic}
if is_emoji_extended_pictographic(this_rune) {
if current_sequence != .Emoji || last_rune != ZERO_WIDTH_JOINER {
grapheme_count += 1
}
current_sequence = .Emoji
continue_sequence = true
continue
}
// "Do not break within emoji flag sequences.
// That is, do not break between regional indicator (RI) symbols
// if there is an odd number of RI characters before the break point."
//
// GB12: sot (RI RI)* RI × RI
// GB13: [^RI] (RI RI)* RI × RI
if is_regional_indicator(this_rune) {
if regional_indicator_counter & 1 == 0 {
grapheme_count += 1
}
current_sequence = .Regional
continue_sequence = true
regional_indicator_counter += 1
continue
}
// "Otherwise, break everywhere."
//
// GB999: Any ÷ Any
grapheme_count += 1
}
return
}

View File

@@ -121,6 +121,7 @@ import edit "core:text/edit"
import thread "core:thread"
import time "core:time"
import datetime "core:time/datetime"
import flags "core:flags"
import sysinfo "core:sys/info"
@@ -233,6 +234,7 @@ _ :: edit
_ :: thread
_ :: time
_ :: datetime
_ :: flags
_ :: sysinfo
_ :: unicode
_ :: utf8

View File

@@ -1077,7 +1077,7 @@ gb_internal void check_proc_decl(CheckerContext *ctx, Entity *e, DeclInfo *d) {
}
if (e->pkg != nullptr && e->token.string == "main") {
if (e->pkg != nullptr && e->token.string == "main" && !build_context.no_entry_point) {
if (e->pkg->kind != Package_Runtime) {
if (pt->param_count != 0 ||
pt->result_count != 0) {

View File

@@ -9819,7 +9819,9 @@ gb_internal ExprKind check_compound_literal(CheckerContext *c, Operand *o, Ast *
if (tav.mode != Addressing_Constant) {
continue;
}
GB_ASSERT(tav.value.kind == ExactValue_Integer);
if (tav.value.kind != ExactValue_Integer) {
continue;
}
i64 v = big_int_to_i64(&tav.value.value_integer);
i64 lower = bt->BitSet.lower;
u64 index = cast(u64)(v-lower);

View File

@@ -4110,6 +4110,7 @@ gb_internal void check_collect_value_decl(CheckerContext *c, Ast *decl) {
bool is_test = false;
bool is_init = false;
bool is_fini = false;
bool is_priv = false;
for_array(i, vd->attributes) {
Ast *attr = vd->attributes[i];
@@ -4154,6 +4155,8 @@ gb_internal void check_collect_value_decl(CheckerContext *c, Ast *decl) {
}
if (!success) {
error(value, "'%.*s' expects no parameter, or a string literal containing \"file\" or \"package\"", LIT(name));
} else {
is_priv = true;
}
@@ -4175,6 +4178,11 @@ gb_internal void check_collect_value_decl(CheckerContext *c, Ast *decl) {
}
}
if (is_priv && is_test) {
error(decl, "Attribute 'private' is not allowed on a test case");
return;
}
if (entity_visibility_kind == EntityVisiblity_Public &&
(c->scope->flags&ScopeFlag_File) &&
c->scope->file) {

View File

@@ -265,16 +265,20 @@ gb_internal i32 linker_stage(LinkerData *gen) {
if (!build_context.use_lld) { // msvc
String res_path = {};
defer (gb_free(heap_allocator(), res_path.text));
// TODO(Jeroen): Add ability to reuse .res file instead of recompiling, if `-resource:file.res` is given.
if (build_context.has_resource) {
String temp_res_path = path_to_string(heap_allocator(), build_context.build_paths[BuildPath_RES]);
res_path = concatenate3_strings(heap_allocator(), str_lit("\""), temp_res_path, str_lit("\""));
gb_free(heap_allocator(), temp_res_path.text);
String rc_path = path_to_string(heap_allocator(), build_context.build_paths[BuildPath_RC]);
String temp_rc_path = path_to_string(heap_allocator(), build_context.build_paths[BuildPath_RC]);
String rc_path = concatenate3_strings(heap_allocator(), str_lit("\""), temp_rc_path, str_lit("\""));
gb_free(heap_allocator(), temp_rc_path.text);
defer (gb_free(heap_allocator(), rc_path.text));
result = system_exec_command_line_app("msvc-link",
"\"%.*src.exe\" /nologo /fo \"%.*s\" \"%.*s\"",
"\"%.*src.exe\" /nologo /fo %.*s %.*s",
LIT(windows_sdk_bin_path),
LIT(res_path),
LIT(rc_path)

View File

@@ -2934,7 +2934,8 @@ int main(int arg_count, char const **arg_ptr) {
// TODO(jeroen): Remove the `init_filename` param.
// Let's put that on `build_context.build_paths[0]` instead.
if (parse_packages(parser, init_filename) != ParseFile_None) {
return 1;
GB_ASSERT_MSG(any_errors(), "parse_packages failed but no error was reported.");
// We depend on the next conditional block to return 1, after printing errors.
}
if (any_errors()) {

View File

@@ -20,15 +20,12 @@ test_avl :: proc(t: ^testing.T) {
iter := avl.iterator(&tree, avl.Direction.Forward)
testing.expect(t, avl.iterator_get(&iter) == nil, "empty/iterator: first node should be nil")
r: rand.Rand
rand.init(&r, t.seed)
// Test insertion.
NR_INSERTS :: 32 + 1 // Ensure at least 1 collision.
inserted_map := make(map[int]^avl.Node(int))
defer delete(inserted_map)
for i := 0; i < NR_INSERTS; i += 1 {
v := int(rand.uint32(&r) & 0x1f)
v := int(rand.uint32() & 0x1f)
existing_node, in_map := inserted_map[v]
n, ok, _ := avl.find_or_insert(&tree, v)
@@ -78,7 +75,7 @@ test_avl :: proc(t: ^testing.T) {
testing.expect(t, visited == nrEntries, "iterator/backward: visited")
// Test removal.
rand.shuffle(inserted_values[:], &r)
rand.shuffle(inserted_values[:])
for v, i in inserted_values {
node := avl.find(&tree, v)
testing.expect(t, node != nil, "remove: find (pre)")

View File

@@ -14,9 +14,6 @@ test_rbtree_integer :: proc(t: ^testing.T, $Key: typeid, $Value: typeid) {
defer mem.tracking_allocator_destroy(&track)
context.allocator = mem.tracking_allocator(&track)
r: rand.Rand
rand.init(&r, t.seed)
log.infof("Testing Red-Black Tree($Key=%v,$Value=%v) using random seed %v.", type_info_of(Key), type_info_of(Value), t.seed)
tree: rb.Tree(Key, Value)
rb.init(&tree)
@@ -35,9 +32,9 @@ test_rbtree_integer :: proc(t: ^testing.T, $Key: typeid, $Value: typeid) {
max_key := min(Key)
for i := 0; i < NR_INSERTS; i += 1 {
k := Key(rand.uint32(&r)) & 0x1f
k := Key(rand.uint32()) & 0x1f
min_key = min(min_key, k); max_key = max(max_key, k)
v := Value(rand.uint32(&r))
v := Value(rand.uint32())
existing_node, in_map := inserted_map[k]
n, inserted, _ := rb.find_or_insert(&tree, k, v)
@@ -92,7 +89,7 @@ test_rbtree_integer :: proc(t: ^testing.T, $Key: typeid, $Value: typeid) {
testing.expect(t, visited == entry_count, "iterator/backward: visited")
// Test removal (and on_remove callback)
rand.shuffle(inserted_keys[:], &r)
rand.shuffle(inserted_keys[:])
callback_count := entry_count
tree.user_data = &callback_count
tree.on_remove = proc(key: Key, value: Value, user_data: rawptr) {

File diff suppressed because it is too large Load Diff

View File

@@ -53,9 +53,9 @@ test_xxhash_zero_streamed_random_updates :: proc(t: ^testing.T) {
testing.expect(t, xxh3_128_err == nil, "Problem initializing XXH3_128 state")
// XXH3_128_update
random_seed := rand.create(t.seed)
rand.reset(t.seed)
for len(b) > 0 {
update_size := min(len(b), rand.int_max(8192, &random_seed))
update_size := min(len(b), rand.int_max(8192))
if update_size > 4096 {
update_size %= 73
}

View File

@@ -0,0 +1,35 @@
package test_core_math_rand
import "core:math/rand"
import "core:testing"
@test
test_default_rand_determinism :: proc(t: ^testing.T) {
rand.reset(13)
first_value := rand.int127()
rand.reset(13)
second_value := rand.int127()
testing.expect(t, first_value == second_value, "Context default random number generator is non-deterministic.")
}
@test
test_default_rand_determinism_user_set :: proc(t: ^testing.T) {
rng_state_1 := rand.create(13)
rng_state_2 := rand.create(13)
rng_1 := rand.default_random_generator(&rng_state_1)
rng_2 := rand.default_random_generator(&rng_state_2)
first_value, second_value: i128
{
context.random_generator = rng_1
first_value = rand.int127()
}
{
context.random_generator = rng_2
second_value = rand.int127()
}
testing.expect(t, first_value == second_value, "User-set default random number generator is non-deterministic.")
}

View File

@@ -19,11 +19,13 @@ download_assets :: proc() {
@(require) import "encoding/json"
@(require) import "encoding/varint"
@(require) import "encoding/xml"
@(require) import "flags"
@(require) import "fmt"
@(require) import "math"
@(require) import "math/big"
@(require) import "math/linalg/glsl"
@(require) import "math/noise"
@(require) import "math/rand"
@(require) import "mem"
@(require) import "net"
@(require) import "odin"
@@ -37,3 +39,4 @@ download_assets :: proc() {
@(require) import "text/match"
@(require) import "thread"
@(require) import "time"
@(require) import "unicode"

View File

@@ -3,6 +3,7 @@ package test_core_slice
import "core:slice"
import "core:testing"
import "core:math/rand"
import "core:log"
@test
test_sort_with_indices :: proc(t: ^testing.T) {
@@ -10,7 +11,7 @@ test_sort_with_indices :: proc(t: ^testing.T) {
test_sizes :: []int{7, 13, 347, 1031, 10111, 100003}
for test_size in test_sizes {
r := rand.create(t.seed)
rand.reset(t.seed)
vals := make([]u64, test_size)
r_idx := make([]int, test_size) // Reverse index
@@ -21,7 +22,7 @@ test_sort_with_indices :: proc(t: ^testing.T) {
// Set up test values
for _, i in vals {
vals[i] = rand.uint64(&r)
vals[i] = rand.uint64()
}
// Sort
@@ -29,7 +30,7 @@ test_sort_with_indices :: proc(t: ^testing.T) {
defer delete(f_idx)
// Verify sorted test values
rand.init(&r, t.seed)
rand.reset(t.seed)
for v, i in f_idx {
r_idx[v] = i
@@ -45,7 +46,7 @@ test_sort_with_indices :: proc(t: ^testing.T) {
}
}
idx_pass := vals[r_idx[i]] == rand.uint64(&r)
idx_pass := vals[r_idx[i]] == rand.uint64()
testing.expect(t, idx_pass, "Expected index to have been sorted")
if !idx_pass {
break
@@ -61,7 +62,7 @@ test_sort_by_indices :: proc(t: ^testing.T) {
test_sizes :: []int{7, 13, 347, 1031, 10111, 100003}
for test_size in test_sizes {
r := rand.create(t.seed)
rand.reset(t.seed)
vals := make([]u64, test_size)
r_idx := make([]int, test_size) // Reverse index
@@ -72,7 +73,7 @@ test_sort_by_indices :: proc(t: ^testing.T) {
// Set up test values
for _, i in vals {
vals[i] = rand.uint64(&r)
vals[i] = rand.uint64()
}
// Sort
@@ -80,7 +81,7 @@ test_sort_by_indices :: proc(t: ^testing.T) {
defer delete(f_idx)
// Verify sorted test values
rand.init(&r, t.seed)
rand.reset(t.seed)
{
indices := make([]int, test_size)
@@ -200,12 +201,12 @@ test_permutation_iterator :: proc(t: ^testing.T) {
permutations_counted: int
for slice.permute(&iter) {
n := 0
for item, index in s {
for item in s {
n *= 10
n += item
}
if n in seen {
testing.fail_now(t, "Permutation iterator made a duplicate permutation.")
log.error("Permutation iterator made a duplicate permutation.")
return
}
seen[n] = true
@@ -215,3 +216,63 @@ test_permutation_iterator :: proc(t: ^testing.T) {
testing.expect_value(t, len(seen), FAC_5)
testing.expect_value(t, permutations_counted, FAC_5)
}
// Test inputs from #3276 and #3769
UNIQUE_TEST_VECTORS :: [][2][]int{
{{2,2,2}, {2}},
{{1,1,1,2,2,3,3,3,3}, {1,2,3}},
{{1,2,4,4,5}, {1,2,4,5}},
}
@test
test_unique :: proc(t: ^testing.T) {
for v in UNIQUE_TEST_VECTORS {
assorted := v[0]
expected := v[1]
uniq := slice.unique(assorted)
testing.expectf(t, slice.equal(uniq, expected), "Expected slice.uniq(%v) == %v, got %v", v[0], v[1], uniq)
}
for v in UNIQUE_TEST_VECTORS {
assorted := v[0]
expected := v[1]
uniq := slice.unique_proc(assorted, proc(a, b: int) -> bool {
return a == b
})
testing.expectf(t, slice.equal(uniq, expected), "Expected slice.unique_proc(%v, ...) == %v, got %v", v[0], v[1], uniq)
}
r := rand.create(t.seed)
context.random_generator = rand.default_random_generator(&r)
// 10_000 random tests
for _ in 0..<10_000 {
assorted: [dynamic]i64
expected: [dynamic]i64
// Prime with 1 value
old := rand.int63()
append(&assorted, old)
append(&expected, old)
// Add 99 additional random values
for _ in 1..<100 {
new := rand.int63()
append(&assorted, new)
if old != new {
append(&expected, new)
}
old = new
}
original := slice.clone(assorted[:])
uniq := slice.unique(assorted[:])
testing.expectf(t, slice.equal(uniq, expected[:]), "Expected slice.uniq(%v) == %v, got %v", original, expected, uniq)
delete(assorted)
delete(original)
delete(expected)
}
}

View File

@@ -5,6 +5,7 @@ import "core:testing"
import "core:text/i18n"
T :: i18n.get
Tn :: i18n.get_n
Test :: struct {
section: string,
@@ -47,7 +48,8 @@ test_custom_pluralizer :: proc(t: ^testing.T) {
{"", "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.
// This isn't in the catalog, so should return the key.
{"", "Come visit us on Discord!", "Come visit us on Discord!", 1},
},
})
@@ -61,11 +63,11 @@ test_mixed_context :: proc(t: ^testing.T) {
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},
{"", "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},
{"", "Come visit us on Discord!", "Come visit us on Discord!", -1},
},
})
}
@@ -90,15 +92,15 @@ test_nl_mo :: proc(t: ^testing.T) {
plural = nil, // Default pluralizer
tests = {
// These are in the catalog.
{"", "There are 69,105 leaves here.", "Er zijn hier 69.105 bladeren.", 1},
{"", "Hellope, World!", "Hallo, Wereld!", 1},
{"", "There are 69,105 leaves here.", "Er zijn hier 69.105 bladeren.", -1},
{"", "Hellope, World!", "Hallo, Wereld!", -1},
{"", "There is %d leaf.\n", "Er is %d blad.\n", 1},
{"", "There are %d leaves.\n", "Er is %d blad.\n", 1},
{"", "There is %d leaf.\n", "Er zijn %d bladeren.\n", 42},
{"", "There are %d leaves.\n", "Er zijn %d bladeren.\n", 42},
// This isn't in the catalog, so should ruturn the key.
{"", "Come visit us on Discord!", "Come visit us on Discord!", 1},
{"", "Come visit us on Discord!", "Come visit us on Discord!", -1},
},
})
}
@@ -111,15 +113,15 @@ test_qt_linguist :: proc(t: ^testing.T) {
plural = nil, // Default pluralizer
tests = {
// These are in the catalog.
{"Page", "Text for translation", "Tekst om te vertalen", 1},
{"Page", "Also text to translate", "Ook tekst om te vertalen", 1},
{"installscript", "99 bottles of beer on the wall", "99 flessen bier op de muur", 1},
{"Page", "Text for translation", "Tekst om te vertalen", -1},
{"Page", "Also text to translate", "Ook tekst om te vertalen", -1},
{"installscript", "99 bottles of beer on the wall", "99 flessen bier op de muur", -1},
{"apple_count", "%d apple(s)", "%d appel", 1},
{"apple_count", "%d apple(s)", "%d appels", 42},
// These aren't in the catalog, so should ruturn the key.
{"", "Come visit us on Discord!", "Come visit us on Discord!", 1},
{"Fake_Section", "Come visit us on Discord!", "Come visit us on Discord!", 1},
{"", "Come visit us on Discord!", "Come visit us on Discord!", -1},
{"Fake_Section", "Come visit us on Discord!", "Come visit us on Discord!", -1},
},
})
}
@@ -133,16 +135,16 @@ test_qt_linguist_merge_sections :: proc(t: ^testing.T) {
options = {merge_sections = true},
tests = {
// All of them are now in section "", lookup with original section should return the key.
{"", "Text for translation", "Tekst om te vertalen", 1},
{"", "Also text to translate", "Ook tekst om te vertalen", 1},
{"", "99 bottles of beer on the wall", "99 flessen bier op de muur", 1},
{"", "Text for translation", "Tekst om te vertalen", -1},
{"", "Also text to translate", "Ook tekst om te vertalen", -1},
{"", "99 bottles of beer on the wall", "99 flessen bier op de muur", -1},
{"", "%d apple(s)", "%d appel", 1},
{"", "%d apple(s)", "%d appels", 42},
// All of them are now in section "", lookup with original section should return the key.
{"Page", "Text for translation", "Text for translation", 1},
{"Page", "Also text to translate", "Also text to translate", 1},
{"installscript", "99 bottles of beer on the wall", "99 bottles of beer on the wall", 1},
{"Page", "Text for translation", "Text for translation", -1},
{"Page", "Also text to translate", "Also text to translate", -1},
{"installscript", "99 bottles of beer on the wall", "99 bottles of beer on the wall", -1},
{"apple_count", "%d apple(s)", "%d apple(s)", 1},
{"apple_count", "%d apple(s)", "%d apple(s)", 42},
},
@@ -175,7 +177,7 @@ test :: proc(t: ^testing.T, suite: Test_Suite, loc := #caller_location) {
if err == .None {
for test in suite.tests {
val := T(test.section, test.key, test.n, cat)
val := test.n > -1 ? Tn(test.section, test.key, test.n, cat): T(test.section, test.key, cat)
testing.expectf(t, val == test.val, "Expected key `%v` from section `%v`'s form for value `%v` to equal `%v`, got `%v`", test.key, test.section, test.n, test.val, val, loc=loc)
}
}

View File

@@ -0,0 +1,73 @@
package test_core_unicode
import "core:log"
import "core:testing"
import "core:unicode/utf8"
Test_Case :: struct {
str: string,
expected_clusters: int,
}
run_test_cases :: proc(t: ^testing.T, test_cases: []Test_Case, loc := #caller_location) {
failed := 0
for c, i in test_cases {
log.debugf("(#% 4i) %q ...", i, c.str)
result, _ := utf8.grapheme_count(c.str)
if !testing.expectf(t, result == c.expected_clusters,
"(#% 4i) graphemes: %i != %i, %q %s", i, result, c.expected_clusters, c.str, c.str,
loc = loc)
{
failed += 1
}
}
log.logf(.Error if failed > 0 else .Info, "% 4i/% 4i test cases failed.", failed, len(test_cases), location = loc)
}
@test
test_official_gcb_cases :: proc(t: ^testing.T) {
run_test_cases(t, official_grapheme_break_test_cases)
}
@test
test_official_emoji_cases :: proc(t: ^testing.T) {
run_test_cases(t, official_emoji_test_cases)
}
@test
test_grapheme_byte_index_segmentation :: proc(t: ^testing.T) {
SAMPLE_1 :: "\U0001F600"
SAMPLE_2 :: "\U0001F3F4\U000E0067\U000E0062\U000E0065\U000E006E\U000E0067\U000E007F"
SAMPLE_3 :: "\U0001F468\U0001F3FB\u200D\U0001F9B0"
str := SAMPLE_1 + SAMPLE_2 + SAMPLE_3 + SAMPLE_2 + SAMPLE_1
graphemes, _, _ := utf8.decode_grapheme_clusters(str)
defer delete(graphemes)
defer if testing.failed(t) {
log.infof("%#v\n%q\n%v", graphemes, str, transmute([]u8)str)
}
if !testing.expect_value(t, len(graphemes), 5) {
return
}
testing.expect_value(t, graphemes[0].rune_index, 0)
testing.expect_value(t, graphemes[1].rune_index, 1)
testing.expect_value(t, graphemes[2].rune_index, 8)
testing.expect_value(t, graphemes[3].rune_index, 12)
testing.expect_value(t, graphemes[4].rune_index, 19)
grapheme_1 := str[graphemes[0].byte_index:graphemes[1].byte_index]
grapheme_2 := str[graphemes[1].byte_index:graphemes[2].byte_index]
grapheme_3 := str[graphemes[2].byte_index:graphemes[3].byte_index]
grapheme_4 := str[graphemes[3].byte_index:graphemes[4].byte_index]
grapheme_5 := str[graphemes[4].byte_index:]
testing.expectf(t, grapheme_1 == SAMPLE_1, "expected %q, got %q", SAMPLE_1, grapheme_1)
testing.expectf(t, grapheme_2 == SAMPLE_2, "expected %q, got %q", SAMPLE_2, grapheme_2)
testing.expectf(t, grapheme_3 == SAMPLE_3, "expected %q, got %q", SAMPLE_3, grapheme_3)
testing.expectf(t, grapheme_4 == SAMPLE_2, "expected %q, got %q", SAMPLE_2, grapheme_2)
testing.expectf(t, grapheme_5 == SAMPLE_1, "expected %q, got %q", SAMPLE_1, grapheme_1)
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,10 +16,10 @@ map_insert_random_key_value :: proc(t: ^testing.T) {
defer delete(m)
unique_keys := 0
r := rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
for _ in 0..<entries {
k := rand.int63(&r)
v := rand.int63(&r)
k := rand.int63()
v := rand.int63()
if k not_in m {
unique_keys += 1
@@ -36,12 +36,12 @@ map_insert_random_key_value :: proc(t: ^testing.T) {
testing.expectf(t, len(m) == unique_keys, "Expected len(map) to equal %v, got %v", unique_keys, len(m))
// Reset randomizer and verify
r = rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
num_fails := 0
for _ in 0..<entries {
k := rand.int63(&r)
v := rand.int63(&r)
k := rand.int63()
v := rand.int63()
cond := m[k] == v
if !cond {
@@ -66,10 +66,11 @@ map_update_random_key_value :: proc(t: ^testing.T) {
defer delete(m)
unique_keys := 0
r := rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
for _ in 0..<entries {
k := rand.int63(&r)
v := rand.int63(&r)
k := rand.int63()
v := rand.int63()
if k not_in m {
unique_keys += 1
@@ -88,21 +89,22 @@ map_update_random_key_value :: proc(t: ^testing.T) {
half_entries := entries / 2
// Reset randomizer and update half the entries
r = rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
for _ in 0..<half_entries {
k := rand.int63(&r)
v := rand.int63(&r)
k := rand.int63()
v := rand.int63()
m[k] = v + 42
}
// Reset randomizer and verify
r = rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
num_fails := 0
for i in 0..<entries {
k := rand.int63(&r)
v := rand.int63(&r)
k := rand.int63()
v := rand.int63()
diff := i64(42) if i < half_entries else i64(0)
cond := m[k] == (v + diff)
@@ -128,10 +130,11 @@ map_delete_random_key_value :: proc(t: ^testing.T) {
defer delete(m)
unique_keys := 0
r := rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
for _ in 0..<entries {
k := rand.int63(&r)
v := rand.int63(&r)
k := rand.int63()
v := rand.int63()
if k not_in m {
unique_keys += 1
@@ -150,21 +153,22 @@ map_delete_random_key_value :: proc(t: ^testing.T) {
half_entries := entries / 2
// Reset randomizer and delete half the entries
r = rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
for _ in 0..<half_entries {
k := rand.int63(&r)
_ = rand.int63(&r)
k := rand.int63()
_ = rand.int63()
delete_key(&m, k)
}
// Reset randomizer and verify
r = rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
num_fails := 0
for i in 0..<entries {
k := rand.int63(&r)
v := rand.int63(&r)
k := rand.int63()
v := rand.int63()
if i < half_entries {
if k in m {
@@ -206,9 +210,10 @@ set_insert_random_key_value :: proc(t: ^testing.T) {
defer delete(m)
unique_keys := 0
r := rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
for _ in 0..<entries {
k := rand.int63(&r)
k := rand.int63()
if k not_in m {
unique_keys += 1
}
@@ -224,11 +229,11 @@ set_insert_random_key_value :: proc(t: ^testing.T) {
testing.expectf(t, len(m) == unique_keys, "Expected len(map) to equal %v, got %v", unique_keys, len(m))
// Reset randomizer and verify
r = rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
num_fails := 0
for _ in 0..<entries {
k := rand.int63(&r)
k := rand.int63()
cond := k in m
if !cond {
@@ -253,9 +258,10 @@ set_delete_random_key_value :: proc(t: ^testing.T) {
defer delete(m)
unique_keys := 0
r := rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
for _ in 0..<entries {
k := rand.int63(&r)
k := rand.int63()
if k not_in m {
unique_keys += 1
@@ -274,18 +280,19 @@ set_delete_random_key_value :: proc(t: ^testing.T) {
half_entries := entries / 2
// Reset randomizer and delete half the entries
r = rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
for _ in 0..<half_entries {
k := rand.int63(&r)
k := rand.int63()
delete_key(&m, k)
}
// Reset randomizer and verify
r = rand.create(t.seed + seed_incr)
rand.reset(t.seed + seed_incr)
num_fails := 0
for i in 0..<entries {
k := rand.int63(&r)
k := rand.int63()
if i < half_entries {
if k in m {