Merge remote-tracking branch 'offical/master'

# Conflicts:
#	src/check_builtin.cpp
This commit is contained in:
2024-06-20 08:58:03 -04:00
88 changed files with 18514 additions and 373 deletions
+1 -1
View File
@@ -303,7 +303,7 @@ bin/
# - Linux/MacOS
odin
!odin/
odin.dSYM
**/*.dSYM
*.bin
demo.bin
libLLVM*.so*
+2
View File
@@ -73,6 +73,8 @@ expect :: proc(val, expected_val: T) -> T ---
// Linux and Darwin Only
syscall :: proc(id: uintptr, args: ..uintptr) -> uintptr ---
// FreeBSD, NetBSD, et cetera
syscall_bsd :: proc(id: uintptr, args: ..uintptr) -> (uintptr, bool) ---
// Atomics
@@ -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,
+6 -1
View File
@@ -73,7 +73,12 @@ default_random_generator_proc :: proc(data: rawptr, mode: Random_Generator_Mode,
_ = read_u64(r)
}
r := &global_rand_seed
r: ^Default_Random_State = ---
if data == nil {
r = &global_rand_seed
} else {
r = cast(^Default_Random_State)data
}
switch mode {
case .Read:
+1 -1
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)
}
+1 -1
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)
}
+28
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
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
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
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,
}
+9
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,
}
+11
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,
}
+132
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")
}
}
+262
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),
}
}
+170
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
}
+536
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
}
+31
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
}
+244
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
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
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
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
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
}
+37
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
}
+10 -4
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 {
+8 -4
View File
@@ -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
}
}
+14
View File
@@ -286,6 +286,20 @@ to_string :: proc(b: Builder) -> (res: string) {
return string(b.buf[:])
}
/*
Appends a trailing null byte after the end of the current Builder byte buffer and then casts it to a cstring
Inputs:
- b: A pointer to builder
Returns:
- res: A cstring of the Builder's buffer
*/
to_cstring :: proc(b: ^Builder) -> (res: cstring) {
append(&b.buf, 0)
pop(&b.buf)
return cstring(raw_data(b.buf))
}
/*
Returns the length of the Builder's buffer, in bytes
Inputs:
+7 -7
View File
@@ -30,8 +30,8 @@ get_last_error :: proc "contextless" () -> int {
}
_futex_wait :: proc "contextless" (futex: ^Futex, expected: u32) -> bool {
if cast(int) intrinsics.syscall(unix.SYS___futex, uintptr(futex), FUTEX_WAIT_PRIVATE, uintptr(expected), 0, 0, 0) == -1 {
switch get_last_error() {
if error, ok := intrinsics.syscall_bsd(unix.SYS___futex, uintptr(futex), FUTEX_WAIT_PRIVATE, uintptr(expected), 0, 0, 0); !ok {
switch error {
case EINTR, EAGAIN:
return true
case:
@@ -45,11 +45,11 @@ _futex_wait_with_timeout :: proc "contextless" (futex: ^Futex, expected: u32, du
if duration <= 0 {
return false
}
if cast(int) intrinsics.syscall(unix.SYS___futex, uintptr(futex), FUTEX_WAIT_PRIVATE, uintptr(expected), cast(uintptr) &Time_Spec{
if error, ok := intrinsics.syscall_bsd(unix.SYS___futex, uintptr(futex), FUTEX_WAIT_PRIVATE, uintptr(expected), cast(uintptr) &Time_Spec{
time_sec = cast(uint)(duration / 1e9),
time_nsec = cast(uint)(duration % 1e9),
}, 0, 0) == -1 {
switch get_last_error() {
}, 0, 0); !ok {
switch error {
case EINTR, EAGAIN:
return true
case ETIMEDOUT:
@@ -62,13 +62,13 @@ _futex_wait_with_timeout :: proc "contextless" (futex: ^Futex, expected: u32, du
}
_futex_signal :: proc "contextless" (futex: ^Futex) {
if cast(int) intrinsics.syscall(unix.SYS___futex, uintptr(futex), FUTEX_WAKE_PRIVATE, 1, 0, 0, 0) == -1 {
if _, ok := intrinsics.syscall_bsd(unix.SYS___futex, uintptr(futex), FUTEX_WAKE_PRIVATE, 1, 0, 0, 0); !ok {
_panic("futex_wake_single failure")
}
}
_futex_broadcast :: proc "contextless" (futex: ^Futex) {
if cast(int) intrinsics.syscall(unix.SYS___futex, uintptr(futex), FUTEX_WAKE_PRIVATE, uintptr(max(i32)), 0, 0, 0) == -1 {
if _, ok := intrinsics.syscall_bsd(unix.SYS___futex, uintptr(futex), FUTEX_WAKE_PRIVATE, uintptr(max(i32)), 0, 0, 0); !ok {
_panic("_futex_wake_all failure")
}
}
+4
View File
@@ -712,3 +712,7 @@ Window_setDelegate :: proc "c" (self: ^Window, delegate: ^WindowDelegate) {
Window_backingScaleFactor :: proc "c" (self: ^Window) -> Float {
return msgSend(Float, self, "backingScaleFactor")
}
@(objc_type=Window, objc_name="setWantsLayer")
Window_setWantsLayer :: proc "c" (self: ^Window, ok: BOOL) {
msgSend(nil, self, "setWantsLayer:", ok)
}
+1 -1
View File
@@ -12,7 +12,7 @@ version_string_buf: [1024]u8
init_os_version :: proc () {
os_version.platform = .FreeBSD
kernel_version_buf: [129]u8
kernel_version_buf: [1024]u8
b := strings.builder_from_bytes(version_string_buf[:])
// Retrieve kernel info using `sysctl`, e.g. FreeBSD 13.1-RELEASE-p2 GENERIC
+4 -3
View File
@@ -5,14 +5,15 @@ import "base:intrinsics"
sysctl :: proc(mib: []i32, val: ^$T) -> (ok: bool) {
mib := mib
result_size := i64(size_of(T))
result_size := u64(size_of(T))
res := intrinsics.syscall(SYS_sysctl,
res: uintptr
res, ok = intrinsics.syscall_bsd(SYS_sysctl,
uintptr(raw_data(mib)), uintptr(len(mib)),
uintptr(val), uintptr(&result_size),
uintptr(0), uintptr(0),
)
return res == 0
return
}
// See /usr/include/sys/sysctl.h for details
+26 -81
View File
@@ -38,16 +38,30 @@ foreign kernel32 {
lpNumberOfCharsWritten: LPDWORD,
lpReserved: LPVOID) -> BOOL ---
PeekConsoleInputW :: proc(hConsoleInput: HANDLE,
lpBuffer: ^INPUT_RECORD,
nLength: DWORD,
lpNumberOfEventsRead: LPDWORD) -> BOOL ---
ReadConsoleInputW :: proc(hConsoleInput: HANDLE,
lpBuffer: ^INPUT_RECORD,
nLength: DWORD,
lpNumberOfEventsRead: LPDWORD) -> BOOL ---
GetConsoleMode :: proc(hConsoleHandle: HANDLE,
lpMode: LPDWORD) -> BOOL ---
SetConsoleMode :: proc(hConsoleHandle: HANDLE,
dwMode: DWORD) -> BOOL ---
SetConsoleCursorPosition :: proc(hConsoleHandle: HANDLE,
dwCursorPosition: COORD) -> BOOL ---
dwCursorPosition: COORD) -> BOOL ---
SetConsoleTextAttribute :: proc(hConsoleOutput: HANDLE,
wAttributes: WORD) -> BOOL ---
wAttributes: WORD) -> BOOL ---
GetConsoleCP :: proc() -> UINT ---
SetConsoleCP :: proc(wCodePageID: UINT) -> BOOL ---
GetConsoleOutputCP :: proc() -> UINT ---
SetConsoleOutputCP :: proc(wCodePageID: UINT) -> BOOL ---
FlushConsoleInputBuffer :: proc(hConsoleInput: HANDLE) -> BOOL ---
GetFileInformationByHandle :: proc(hFile: HANDLE, lpFileInformation: LPBY_HANDLE_FILE_INFORMATION) -> BOOL ---
SetHandleInformation :: proc(hObject: HANDLE,
dwMask: DWORD,
@@ -85,6 +99,12 @@ foreign kernel32 {
RemoveDirectoryW :: proc(lpPathName: LPCWSTR) -> BOOL ---
SetFileAttributesW :: proc(lpFileName: LPCWSTR, dwFileAttributes: DWORD) -> BOOL ---
SetLastError :: proc(dwErrCode: DWORD) ---
GetCommTimeouts :: proc(handle: HANDLE, timeouts: ^COMMTIMEOUTS) -> BOOL ---
SetCommTimeouts :: proc(handle: HANDLE, timeouts: ^COMMTIMEOUTS) -> BOOL ---
ClearCommError :: proc(hFile: HANDLE, lpErrors: ^Com_Error, lpStat: ^COMSTAT) -> BOOL ---
GetCommState :: proc(handle: HANDLE, dcb: ^DCB) -> BOOL ---
SetCommState :: proc(handle: HANDLE, dcb: ^DCB) -> BOOL ---
GetCommPorts :: proc(lpPortNumbers: PULONG, uPortNumbersCount: ULONG, puPortNumbersFound: PULONG) -> ULONG ---
GetCommandLineW :: proc() -> LPCWSTR ---
GetTempPathW :: proc(nBufferLength: DWORD, lpBuffer: LPCWSTR) -> DWORD ---
GetCurrentProcess :: proc() -> HANDLE ---
@@ -413,6 +433,7 @@ foreign kernel32 {
LoadLibraryW :: proc(c_str: LPCWSTR) -> HMODULE ---
FreeLibrary :: proc(h: HMODULE) -> BOOL ---
GetProcAddress :: proc(h: HMODULE, c_str: LPCSTR) -> rawptr ---
LoadLibraryExW :: proc(c_str: LPCWSTR, file: HANDLE, flags: LoadLibraryEx_Flags) -> HMODULE ---
GetFullPathNameW :: proc(filename: LPCWSTR, buffer_length: DWORD, buffer: LPCWSTR, file_part: ^LPCWSTR) -> DWORD ---
@@ -1015,82 +1036,6 @@ 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.
DTR_Control :: enum byte {
Disable = 0,
Enable = 1,
Handshake = 2,
}
RTS_Control :: enum byte {
Disable = 0,
Enable = 1,
Handshake = 2,
Toggle = 3,
}
Parity :: enum byte {
None = 0,
Odd = 1,
Even = 2,
Mark = 3,
Space = 4,
}
Stop_Bits :: enum byte {
One = 0,
One_And_A_Half = 1,
Two = 2,
}
DCB :: struct {
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,
}
@(default_calling_convention="system")
foreign kernel32 {
GetCommState :: proc(handle: HANDLE, dcb: ^DCB) -> BOOL ---
SetCommState :: proc(handle: HANDLE, dcb: ^DCB) -> BOOL ---
}
COMMTIMEOUTS :: struct {
ReadIntervalTimeout: DWORD,
ReadTotalTimeoutMultiplier: DWORD,
ReadTotalTimeoutConstant: DWORD,
WriteTotalTimeoutMultiplier: DWORD,
WriteTotalTimeoutConstant: DWORD,
}
@(default_calling_convention="system")
foreign kernel32 {
GetCommTimeouts :: proc(handle: HANDLE, timeouts: ^COMMTIMEOUTS) -> BOOL ---
SetCommTimeouts :: proc(handle: HANDLE, timeouts: ^COMMTIMEOUTS) -> BOOL ---
}
LPFIBER_START_ROUTINE :: #type proc "system" (lpFiberParameter: LPVOID)
@(default_calling_convention = "system")
@@ -1167,9 +1112,9 @@ Battery_Flag :: enum BYTE {
Low = 1,
Critical = 2,
Charging = 3,
No_Battery = 7,
No_Battery = 7,
}
Battery_Flags :: bit_set[Battery_Flag; BYTE]
Battery_Flags :: bit_set[Battery_Flag; BYTE]
/* Global Memory Flags */
GMEM_FIXED :: 0x0000
+169
View File
@@ -2674,6 +2674,22 @@ OSVERSIONINFOEXW :: struct {
wReserved: UCHAR,
}
LoadLibraryEx_Flag :: enum DWORD {
LOAD_LIBRARY_AS_DATAFILE = 1, // 1 << 1: 0x0002,
LOAD_WITH_ALTERED_SEARCH_PATH = 3, // 1 << 3: 0x0008,
LOAD_IGNORE_CODE_AUTHZ_LEVEL = 4, // 1 << 4: 0x0010,
LOAD_LIBRARY_AS_IMAGE_RESOURCE = 5, // 1 << 5: 0x0020,
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 6, // 1 << 6: 0x0040,
LOAD_LIBRARY_REQUIRE_SIGNED_TARGET = 7, // 1 << 7: 0x0080,
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 8, // 1 << 8: 0x0100,
LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 9, // 1 << 9: 0x0200,
LOAD_LIBRARY_SEARCH_USER_DIRS = 10, // 1 << 10: 0x0400,
LOAD_LIBRARY_SEARCH_SYSTEM32 = 11, // 1 << 11: 0x0800,
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 12, // 1 << 12: 0x1000,
LOAD_LIBRARY_SAFE_CURRENT_DIRS = 13, // 1 << 13: 0x2000,
}
LoadLibraryEx_Flags :: distinct bit_set[LoadLibraryEx_Flag]
// https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-quota_limits
// Used in LogonUserExW
PQUOTA_LIMITS :: struct {
@@ -3983,6 +3999,70 @@ CONSOLE_CURSOR_INFO :: struct {
PCONSOLE_SCREEN_BUFFER_INFO :: ^CONSOLE_SCREEN_BUFFER_INFO
PCONSOLE_CURSOR_INFO :: ^CONSOLE_CURSOR_INFO
Event_Type :: enum WORD {
KEY_EVENT = 0x0001,
MOUSE_EVENT = 0x0002,
WINDOW_BUFFER_SIZE_EVENT = 0x0004,
MENU_EVENT = 0x0008,
FOCUS_EVENT = 0x0010,
}
INPUT_RECORD :: struct {
EventType: Event_Type,
Event: struct #raw_union {
KeyEvent: KEY_EVENT_RECORD,
MouseEvent: MOUSE_EVENT_RECORD,
WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD,
MenuEvent: MENU_EVENT_RECORD,
FocusEvent: FOCUS_EVENT_RECORD,
},
}
Control_Key_State_Bits :: enum {
RIGHT_ALT_PRESSED,
LEFT_ALT_PRESSED,
RIGHT_CTRL_PRESSED,
LEFT_CTRL_PRESSED,
SHIFT_PRESSED,
NUMLOCK_ON,
SCROLLLOCK_ON,
CAPSLOCK_ON,
ENHANCED_KEY,
}
Control_Key_State :: bit_set[Control_Key_State_Bits; DWORD]
KEY_EVENT_RECORD :: struct {
bKeyDown: BOOL,
wRepeatCount: WORD,
wVirtualKeyCode: WORD,
wVirtualScanCode: WORD,
uChar: struct #raw_union {
UnicodeChar: WCHAR,
AsciiChar: CHAR,
},
dwControlKeyState: Control_Key_State,
}
MOUSE_EVENT_RECORD :: struct {
dwMousePosition: COORD,
dwButtonState: DWORD,
dwControlKeyState: DWORD,
dwEventFlags: DWORD,
}
WINDOW_BUFFER_SIZE_RECORD :: struct {
dwSize: COORD,
}
MENU_EVENT_RECORD :: struct {
dwCommandId: UINT,
}
FOCUS_EVENT_RECORD :: struct {
bSetFocus: BOOL,
}
//
// Networking
//
@@ -4217,3 +4297,92 @@ SOCKADDR :: struct {
sa_family: ADDRESS_FAMILY,
sa_data: [14]CHAR,
}
DTR_Control :: enum byte {
Disable = 0,
Enable = 1,
Handshake = 2,
}
RTS_Control :: enum byte {
Disable = 0,
Enable = 1,
Handshake = 2,
Toggle = 3,
}
Parity :: enum byte {
None = 0,
Odd = 1,
Even = 2,
Mark = 3,
Space = 4,
}
Stop_Bits :: enum byte {
One = 0,
One_And_A_Half = 1,
Two = 2,
}
DCB :: struct {
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,
}
COMMTIMEOUTS :: struct {
ReadIntervalTimeout: DWORD,
ReadTotalTimeoutMultiplier: DWORD,
ReadTotalTimeoutConstant: DWORD,
WriteTotalTimeoutMultiplier: DWORD,
WriteTotalTimeoutConstant: DWORD,
}
Com_Stat_Bits :: enum {
fCtsHold,
fDsrHold,
fRlsdHold,
fXoffHold,
fXoffSent,
fEof,
fTxim,
}
COMSTAT :: struct {
bits: bit_set[Com_Stat_Bits; DWORD],
cbInQue: DWORD,
cbOutQue: DWORD,
}
Com_Error_Bits :: enum {
RXOVER,
OVERRUN,
RXPARITY,
FRAME,
BREAK,
}
Com_Error :: bit_set[Com_Error_Bits; DWORD]
+8
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"
@@ -106,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)
+40 -38
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
+118 -70
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.
*/
+301 -4
View File
@@ -5,6 +5,12 @@ REPLACEMENT_CHAR :: '\ufffd' // Represented an invalid code point
MAX_ASCII :: '\u007f' // Maximum ASCII value
MAX_LATIN1 :: '\u00ff' // Maximum Latin-1 value
ZERO_WIDTH_SPACE :: '\u200B'
ZERO_WIDTH_NON_JOINER :: '\u200C'
ZERO_WIDTH_JOINER :: '\u200D'
WORD_JOINER :: '\u2060'
@(require_results)
binary_search :: proc(c: i32, table: []i32, length, stride: int) -> int {
n := length
t := 0
@@ -24,6 +30,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 +43,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 +56,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 +67,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 +84,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 +102,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 +123,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 +138,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 +155,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 +176,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 +184,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 +192,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 +200,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 +208,285 @@ 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)
}
// Return values:
//
// - 2 if East_Asian_Width=F or W, or
// - 0 if non-printable / zero-width, or
// - 1 in all other cases.
//
@(require_results)
normalized_east_asian_width :: proc(r: rune) -> int {
// This is a different interpretation of the BOM which occurs in the middle of text.
ZERO_WIDTH_NO_BREAK_SPACE :: '\uFEFF'
if is_control(r) {
return 0
} else if r <= 0x10FF {
// Easy early out for low runes.
return 1
}
switch r {
case ZERO_WIDTH_NO_BREAK_SPACE,
ZERO_WIDTH_SPACE,
ZERO_WIDTH_NON_JOINER,
ZERO_WIDTH_JOINER,
WORD_JOINER:
return 0
}
c := i32(r)
p := binary_search(c, normalized_east_asian_width_ranges[:], len(normalized_east_asian_width_ranges)/3, 3)
if p >= 0 && normalized_east_asian_width_ranges[p] <= c && c <= normalized_east_asian_width_ranges[p+1] {
return cast(int)normalized_east_asian_width_ranges[p+2]
}
return 1
}
//
// End of Unicode 15.1.0 block.
//
File diff suppressed because it is too large Load Diff
+420
View File
@@ -0,0 +1,420 @@
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
normalized_east_asian_width :: unicode.normalized_east_asian_width
Grapheme :: struct {
byte_index: int,
rune_index: int,
width: 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.
- width: The width of the string in number of monospace cells.
*/
@(require_results)
grapheme_count :: proc(str: string) -> (graphemes, runes, width: int) {
_, graphemes, runes, width = 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.
- width: The width of the string in number of monospace cells.
*/
@(require_results)
decode_grapheme_clusters :: proc(
str: string,
track_graphemes := true,
allocator := context.allocator,
) -> (
graphemes: [dynamic]Grapheme,
grapheme_count: int,
rune_count: int,
width: 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
// Additionally, this procedure now takes into account Standard Annex #11,
// in order to estimate how visually wide the string will appear on a
// monospaced display. This can only ever be a rough guess, as this tends
// to be an implementation detail relating to which fonts are being used,
// how codepoints are interpreted and drawn, if codepoint sequences are
// interpreted correctly, and et cetera.
//
// For example, a program may not properly interpret an emoji modifier
// sequence and print the component glyphs instead of one whole glyph.
//
// See here for more information: https://www.unicode.org/reports/tr11/
//
// NOTE: There is no explicit mention of what to do with zero-width spaces
// as far as grapheme cluster segmentation goes, therefore this
// implementation may count and return graphemes with a `width` of zero.
//
// Treat them as any other space.
Grapheme_Cluster_Sequence :: enum {
None,
Indic,
Emoji,
Regional,
}
context.allocator = allocator
last_rune: rune
last_rune_breaks_forward: bool
last_width: int
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 grapheme_count > last_grapheme_count {
width += normalized_east_asian_width(this_rune)
if track_graphemes {
append(&graphemes, Grapheme{
byte_index,
rune_count,
width - last_width,
})
}
last_grapheme_count = grapheme_count
last_width = width
}
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
}
+2
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
+4
View File
@@ -0,0 +1,4 @@
package all
import wgpu "vendor:wgpu"
_ :: wgpu
+2
View File
@@ -844,6 +844,8 @@ struct BuildContext {
bool show_unused;
bool show_unused_with_location;
bool show_more_timings;
bool show_defineables;
String export_defineables_file;
bool show_system_calls;
bool keep_temp_files;
bool ignore_unknown_attributes;
+81 -9
View File
@@ -1756,9 +1756,21 @@ gb_internal bool check_builtin_procedure_directive(CheckerContext *c, Operand *o
operand->mode = Addressing_Constant;
operand->value = exact_value_bool(is_defined);
// If the arg is a selector expression we don't add it, `-define` only allows identifiers.
if (arg->kind == Ast_Ident) {
Defineable defineable = {};
defineable.docs = nullptr;
defineable.name = arg->Ident.token.string;
defineable.default_value = exact_value_bool(false);
defineable.pos = arg->Ident.token.pos;
MUTEX_GUARD(&c->info->defineables_mutex);
array_add(&c->info->defineables, defineable);
}
} else if (name == "config") {
if (ce->args.count != 2) {
error(call, "'#config' expects 2 argument, got %td", ce->args.count);
error(call, "'#config' expects 2 arguments, got %td", ce->args.count);
return false;
}
Ast *arg = unparen_expr(ce->args[0]);
@@ -1793,8 +1805,22 @@ gb_internal bool check_builtin_procedure_directive(CheckerContext *c, Operand *o
operand->value = found->Constant.value;
}
}
}
Defineable defineable = {};
defineable.docs = nullptr;
defineable.name = name;
defineable.default_value = def.value;
defineable.pos = arg->Ident.token.pos;
if (c->decl) {
defineable.docs = c->decl->docs;
}
MUTEX_GUARD(&c->info->defineables_mutex);
array_add(&c->info->defineables, defineable);
}
// Bodging in #region & #endregion support
else if (name == "region") {
operand->type = t_untyped_bool;
@@ -1806,7 +1832,7 @@ gb_internal bool check_builtin_procedure_directive(CheckerContext *c, Operand *o
operand->mode = Addressing_Constant;
operand->value = exact_value_bool(true);
}
else {
error(call, "Unknown directive call: #%.*s", LIT(name));
}
@@ -5103,15 +5129,9 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As
isize max_arg_count = 32;
switch (build_context.metrics.os) {
case TargetOs_windows:
case TargetOs_freestanding:
error(call, "'%.*s' is not supported on this platform (%.*s)", LIT(builtin_name), LIT(target_os_names[build_context.metrics.os]));
break;
case TargetOs_darwin:
case TargetOs_linux:
case TargetOs_essence:
case TargetOs_freebsd:
case TargetOs_openbsd:
case TargetOs_haiku:
switch (build_context.metrics.arch) {
case TargetArch_i386:
@@ -5121,6 +5141,9 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As
break;
}
break;
default:
error(call, "'%.*s' is not supported on this platform (%.*s)", LIT(builtin_name), LIT(target_os_names[build_context.metrics.os]));
break;
}
if (ce->args.count > max_arg_count) {
@@ -5134,6 +5157,55 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As
return true;
}
break;
case BuiltinProc_syscall_bsd:
{
convert_to_typed(c, operand, t_uintptr);
if (!is_type_uintptr(operand->type)) {
gbString t = type_to_string(operand->type);
error(operand->expr, "Argument 0 must be of type 'uintptr', got %s", t);
gb_string_free(t);
}
for (isize i = 1; i < ce->args.count; i++) {
Operand x = {};
check_expr(c, &x, ce->args[i]);
if (x.mode != Addressing_Invalid) {
convert_to_typed(c, &x, t_uintptr);
}
convert_to_typed(c, &x, t_uintptr);
if (!is_type_uintptr(x.type)) {
gbString t = type_to_string(x.type);
error(x.expr, "Argument %td must be of type 'uintptr', got %s", i, t);
gb_string_free(t);
}
}
isize max_arg_count = 32;
switch (build_context.metrics.os) {
case TargetOs_freebsd:
case TargetOs_netbsd:
case TargetOs_openbsd:
switch (build_context.metrics.arch) {
case TargetArch_amd64:
case TargetArch_arm64:
max_arg_count = 7;
break;
}
break;
default:
error(call, "'%.*s' is not supported on this platform (%.*s)", LIT(builtin_name), LIT(target_os_names[build_context.metrics.os]));
break;
}
if (ce->args.count > max_arg_count) {
error(ast_end_token(call), "'%.*s' has a maximum of %td arguments on this platform (%.*s), got %td", LIT(builtin_name), max_arg_count, LIT(target_os_names[build_context.metrics.os]), ce->args.count);
}
operand->mode = Addressing_Value;
operand->type = make_optional_ok_type(t_uintptr);
return true;
}
break;
case BuiltinProc_type_base_type:
+1 -1
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) {
+16 -1
View File
@@ -2606,6 +2606,11 @@ gb_internal void check_unary_expr(CheckerContext *c, Operand *o, Token op, Ast *
if (o->mode == Addressing_Constant) {
Type *type = base_type(o->type);
if (!is_type_constant_type(o->type)) {
if (is_type_array_like(o->type)) {
o->mode = Addressing_Value;
return;
}
gbString xt = type_to_string(o->type);
gbString err_str = expr_to_string(node);
error(op, "Invalid type, '%s', for constant unary expression '%s'", xt, err_str);
@@ -8324,6 +8329,14 @@ gb_internal ExprKind check_basic_directive_expr(CheckerContext *c, Operand *o, A
}
o->type = t_untyped_string;
o->value = exact_value_string(file);
} else if (name == "directory") {
String file = get_file_path_string(bd->token.pos.file_id);
String path = dir_from_path(file);
if (build_context.obfuscate_source_code_locations) {
path = obfuscate_string(path, "D");
}
o->type = t_untyped_string;
o->value = exact_value_string(path);
} else if (name == "line") {
i32 line = bd->token.pos.line;
if (build_context.obfuscate_source_code_locations) {
@@ -9821,7 +9834,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);
+10
View File
@@ -1302,6 +1302,7 @@ gb_internal void init_checker_info(CheckerInfo *i) {
array_init(&i->init_procedures, a, 0, 0);
array_init(&i->fini_procedures, a, 0, 0);
array_init(&i->required_foreign_imports_through_force, a, 0, 0);
array_init(&i->defineables, a);
map_init(&i->objc_msgSend_types);
string_map_init(&i->load_file_cache);
@@ -1331,6 +1332,7 @@ gb_internal void destroy_checker_info(CheckerInfo *i) {
string_map_destroy(&i->packages);
array_free(&i->variable_init_order);
array_free(&i->required_foreign_imports_through_force);
array_free(&i->defineables);
mpsc_destroy(&i->entity_queue);
mpsc_destroy(&i->definition_queue);
@@ -4110,6 +4112,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 +4157,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 +4180,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) {
+14
View File
@@ -382,6 +382,17 @@ struct GenTypesData {
RecursiveMutex mutex;
};
struct Defineable {
String name;
ExactValue default_value;
TokenPos pos;
CommentGroup *docs;
// These strings are only computed from previous fields when defineables are being shown or exported.
String default_value_str;
String pos_str;
};
// CheckerInfo stores all the symbol information for a type-checked program
struct CheckerInfo {
Checker *checker;
@@ -408,6 +419,9 @@ struct CheckerInfo {
Array<Entity *> entities;
Array<Entity *> required_foreign_imports_through_force;
BlockingMutex defineables_mutex;
Array<Defineable> defineables;
// Below are accessed within procedures
RwMutex global_untyped_mutex;
+3 -1
View File
@@ -192,6 +192,7 @@ BuiltinProc__simd_end,
// Platform specific intrinsics
BuiltinProc_syscall,
BuiltinProc_syscall_bsd,
BuiltinProc_x86_cpuid,
BuiltinProc_x86_xgetbv,
@@ -512,7 +513,8 @@ gb_global BuiltinProc builtin_procs[BuiltinProc_COUNT] = {
{STR_LIT(""), 0, false, Expr_Stmt, BuiltinProcPkg_intrinsics},
{STR_LIT("syscall"), 1, true, Expr_Expr, BuiltinProcPkg_intrinsics, false, true},
{STR_LIT("syscall"), 1, true, Expr_Expr, BuiltinProcPkg_intrinsics, false, true},
{STR_LIT("syscall_bsd"), 1, true, Expr_Expr, BuiltinProcPkg_intrinsics, false, true},
{STR_LIT("x86_cpuid"), 2, false, Expr_Expr, BuiltinProcPkg_intrinsics},
{STR_LIT("x86_xgetbv"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics},
+6 -2
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)
+134 -44
View File
@@ -2747,26 +2747,10 @@ gb_internal lbValue lb_build_builtin_proc(lbProcedure *p, Ast *expr, TypeAndValu
{
GB_ASSERT(arg_count <= 7);
// FreeBSD additionally clobbers r8, r9, r10, but they
// can also be used to pass in arguments, so this needs
// to be handled in two parts.
bool clobber_arg_regs[7] = {
false, false, false, false, false, false, false
};
if (build_context.metrics.os == TargetOs_freebsd) {
clobber_arg_regs[4] = true; // r10
clobber_arg_regs[5] = true; // r8
clobber_arg_regs[6] = true; // r9
}
char asm_string[] = "syscall";
gbString constraints = gb_string_make(heap_allocator(), "={rax}");
for (unsigned i = 0; i < arg_count; i++) {
if (!clobber_arg_regs[i]) {
constraints = gb_string_appendc(constraints, ",{");
} else {
constraints = gb_string_appendc(constraints, ",+{");
}
constraints = gb_string_appendc(constraints, ",{");
static char const *regs[] = {
"rax",
"rdi",
@@ -2790,36 +2774,9 @@ gb_internal lbValue lb_build_builtin_proc(lbProcedure *p, Ast *expr, TypeAndValu
// Some but not all system calls will additionally
// clobber memory.
//
// As a fix for CVE-2019-5595, FreeBSD started
// clobbering R8, R9, and R10, instead of restoring
// them. Additionally unlike Linux, instead of
// returning negative errno, positive errno is
// returned and CF is set.
//
// TODO:
// * Figure out what Darwin does.
// * Add some extra handling to propagate CF back
// up to the caller on FreeBSD systems so that
// the caller knows that the return value is
// positive errno.
constraints = gb_string_appendc(constraints, ",~{rcx},~{r11},~{memory}");
if (build_context.metrics.os == TargetOs_freebsd) {
// Second half of dealing with FreeBSD's system
// call semantics. Explicitly clobber the registers
// that were not used to pass in arguments, and
// then clobber RFLAGS.
if (arg_count < 5) {
constraints = gb_string_appendc(constraints, ",~{r10}");
}
if (arg_count < 6) {
constraints = gb_string_appendc(constraints, ",~{r8}");
}
if (arg_count < 7) {
constraints = gb_string_appendc(constraints, ",~{r9}");
}
constraints = gb_string_appendc(constraints, ",~{cc}");
}
inline_asm = llvm_get_inline_asm(func_type, make_string_c(asm_string), make_string_c(constraints));
}
break;
@@ -2927,6 +2884,139 @@ gb_internal lbValue lb_build_builtin_proc(lbProcedure *p, Ast *expr, TypeAndValu
res.type = t_uintptr;
return res;
}
case BuiltinProc_syscall_bsd:
{
// This is a BSD-style syscall where errors are indicated by a high
// Carry Flag and a positive return value, allowing the kernel to
// return any value that fits into a machine word.
//
// This is unlike Linux, where errors are indicated by a negative
// return value, limiting what can be expressed in one result.
unsigned arg_count = cast(unsigned)ce->args.count;
LLVMValueRef *args = gb_alloc_array(permanent_allocator(), LLVMValueRef, arg_count);
for_array(i, ce->args) {
lbValue arg = lb_build_expr(p, ce->args[i]);
arg = lb_emit_conv(p, arg, t_uintptr);
args[i] = arg.value;
}
LLVMTypeRef llvm_uintptr = lb_type(p->module, t_uintptr);
LLVMTypeRef *llvm_arg_types = gb_alloc_array(permanent_allocator(), LLVMTypeRef, arg_count);
for (unsigned i = 0; i < arg_count; i++) {
llvm_arg_types[i] = llvm_uintptr;
}
LLVMTypeRef *results = gb_alloc_array(permanent_allocator(), LLVMTypeRef, 2);
results[0] = lb_type(p->module, t_uintptr);
results[1] = lb_type(p->module, t_bool);
LLVMTypeRef llvm_results = LLVMStructTypeInContext(p->module->ctx, results, 2, false);
LLVMTypeRef func_type = LLVMFunctionType(llvm_results, llvm_arg_types, arg_count, false);
LLVMValueRef inline_asm = nullptr;
switch (build_context.metrics.arch) {
case TargetArch_amd64:
{
GB_ASSERT(arg_count <= 7);
char asm_string[] = "syscall; setnb %cl";
// Using CL as an output; RCX doesn't need to get clobbered later.
gbString constraints = gb_string_make(heap_allocator(), "={rax},={cl}");
for (unsigned i = 0; i < arg_count; i++) {
constraints = gb_string_appendc(constraints, ",{");
static char const *regs[] = {
"rax",
"rdi",
"rsi",
"rdx",
"r10",
"r8",
"r9",
};
constraints = gb_string_appendc(constraints, regs[i]);
constraints = gb_string_appendc(constraints, "}");
}
// NOTE(Feoramund): If you're experiencing instability
// regarding syscalls during optimized builds, it is
// possible that the ABI has changed for your platform,
// or I've missed a register clobber.
//
// Documentation on this topic is sparse, but I was able to
// determine what registers were being clobbered by adding
// dummy values to them, setting a breakpoint after the
// syscall, and checking the state of the registers afterwards.
//
// Be advised that manually stepping through a debugger may
// cause the kernel to not return via sysret, which will
// preserve register state that normally would've been
// otherwise clobbered.
//
// It is also possible that some syscalls clobber different registers.
if (build_context.metrics.os == TargetOs_freebsd) {
// As a fix for CVE-2019-5595, FreeBSD started
// clobbering R8, R9, and R10, instead of restoring
// them.
//
// More info here:
//
// https://www.freebsd.org/security/advisories/FreeBSD-SA-19:01.syscall.asc
// https://github.com/freebsd/freebsd-src/blob/098dbd7ff7f3da9dda03802cdb2d8755f816eada/sys/amd64/amd64/exception.S#L605
// https://stackoverflow.com/q/66878250
constraints = gb_string_appendc(constraints, ",~{r8},~{r9},~{r10}");
}
// Both FreeBSD and NetBSD might clobber RDX.
//
// For NetBSD, it was clobbered during a call to sysctl.
//
// For FreeBSD, it's listed as "return value 2" in their
// AMD64 assembly, so there's no guarantee that it will persist.
constraints = gb_string_appendc(constraints, ",~{rdx},~{r11},~{cc},~{memory}");
inline_asm = llvm_get_inline_asm(func_type, make_string_c(asm_string), make_string_c(constraints));
}
break;
case TargetArch_arm64:
{
GB_ASSERT(arg_count <= 7);
char asm_string[] = "svc #0; cset x8, cc";
gbString constraints = gb_string_make(heap_allocator(), "={x0},={x8}");
for (unsigned i = 0; i < arg_count; i++) {
constraints = gb_string_appendc(constraints, ",{");
static char const *regs[] = {
"x8",
"x0",
"x1",
"x2",
"x3",
"x4",
"x5",
};
constraints = gb_string_appendc(constraints, regs[i]);
constraints = gb_string_appendc(constraints, "}");
}
// FreeBSD clobbered x1 on a call to sysctl.
constraints = gb_string_appendc(constraints, ",~{x1},~{cc},~{memory}");
inline_asm = llvm_get_inline_asm(func_type, make_string_c(asm_string), make_string_c(constraints));
}
break;
default:
GB_PANIC("Unsupported platform");
}
lbValue res = {};
res.value = LLVMBuildCall2(p->builder, func_type, inline_asm, args, arg_count, "");
res.type = make_optional_ok_type(t_uintptr, true);
return res;
}
case BuiltinProc_objc_send:
return lb_handle_objc_send(p, expr);
+156
View File
@@ -288,6 +288,9 @@ enum BuildFlagKind {
BuildFlag_NoThreadedChecker,
BuildFlag_ShowDebugMessages,
BuildFlag_ShowDefineables,
BuildFlag_ExportDefineables,
BuildFlag_Vet,
BuildFlag_VetShadowing,
BuildFlag_VetUnused,
@@ -483,6 +486,9 @@ gb_internal bool parse_build_flags(Array<String> args) {
add_flag(&build_flags, BuildFlag_NoThreadedChecker, str_lit("no-threaded-checker"), BuildFlagParam_None, Command__does_check);
add_flag(&build_flags, BuildFlag_ShowDebugMessages, str_lit("show-debug-messages"), BuildFlagParam_None, Command_all);
add_flag(&build_flags, BuildFlag_ShowDefineables, str_lit("show-defineables"), BuildFlagParam_None, Command__does_check);
add_flag(&build_flags, BuildFlag_ExportDefineables, str_lit("export-defineables"), BuildFlagParam_String, Command__does_check);
add_flag(&build_flags, BuildFlag_Vet, str_lit("vet"), BuildFlagParam_None, Command__does_check);
add_flag(&build_flags, BuildFlag_VetUnused, str_lit("vet-unused"), BuildFlagParam_None, Command__does_check);
add_flag(&build_flags, BuildFlag_VetUnusedVariables, str_lit("vet-unused-variables"), BuildFlagParam_None, Command__does_check);
@@ -814,6 +820,24 @@ gb_internal bool parse_build_flags(Array<String> args) {
break;
}
case BuildFlag_ShowDefineables: {
GB_ASSERT(value.kind == ExactValue_Invalid);
build_context.show_defineables = true;
break;
}
case BuildFlag_ExportDefineables: {
GB_ASSERT(value.kind == ExactValue_String);
String export_path = string_trim_whitespace(value.value_string);
if (is_build_flag_path_valid(export_path)) {
build_context.export_defineables_file = path_to_full_path(heap_allocator(), export_path);
} else {
gb_printf_err("Invalid -export-defineables path, got %.*s\n", LIT(export_path));
bad_flags = true;
}
break;
}
case BuildFlag_ShowSystemCalls: {
GB_ASSERT(value.kind == ExactValue_Invalid);
build_context.show_system_calls = true;
@@ -1553,6 +1577,115 @@ gb_internal void timings_export_all(Timings *t, Checker *c, bool timings_are_fin
gb_printf("Done.\n");
}
gb_internal void check_defines(BuildContext *bc, Checker *c) {
for (auto const &entry : bc->defined_values) {
String name = make_string_c(entry.key);
ExactValue value = entry.value;
GB_ASSERT(value.kind != ExactValue_Invalid);
bool found = false;
for_array(i, c->info.defineables) {
Defineable *def = &c->info.defineables[i];
if (def->name == name) {
found = true;
break;
}
}
if (!found) {
ERROR_BLOCK();
warning(nullptr, "given -define:%.*s is unused in the project", LIT(name));
error_line("\tSuggestion: use the -show-defineables flag for an overview of the possible defines\n");
}
}
}
gb_internal void temp_alloc_defineable_strings(Checker *c) {
for_array(i, c->info.defineables) {
Defineable *def = &c->info.defineables[i];
def->default_value_str = make_string_c(write_exact_value_to_string(gb_string_make(temporary_allocator(), ""), def->default_value));
def->pos_str = make_string_c(token_pos_to_string(def->pos));
}
}
gb_internal GB_COMPARE_PROC(defineables_cmp) {
Defineable *x = (Defineable *)a;
Defineable *y = (Defineable *)b;
int cmp = 0;
String x_file = get_file_path_string(x->pos.file_id);
String y_file = get_file_path_string(y->pos.file_id);
cmp = string_compare(x_file, y_file);
if (cmp) {
return cmp;
}
return i32_cmp(x->pos.offset, y->pos.offset);
}
gb_internal void sort_defineables(Checker *c) {
gb_sort_array(c->info.defineables.data, c->info.defineables.count, defineables_cmp);
}
gb_internal void export_defineables(Checker *c, String path) {
gbFile f = {};
gbFileError err = gb_file_open_mode(&f, gbFileMode_Write, (char *)path.text);
if (err != gbFileError_None) {
gb_printf_err("Failed to export defineables to: %.*s\n", LIT(path));
gb_exit(1);
return;
} else {
gb_printf("Exporting defineables to '%.*s'...\n", LIT(path));
}
defer (gb_file_close(&f));
gbString docs = gb_string_make(heap_allocator(), "");
defer (gb_string_free(docs));
gb_fprintf(&f, "Defineable,Default Value,Docs,Location\n");
for_array(i, c->info.defineables) {
Defineable *def = &c->info.defineables[i];
gb_string_clear(docs);
if (def->docs) {
docs = gb_string_appendc(docs, "\"");
for (Token const &token : def->docs->list) {
for (isize i = 0; i < token.string.len; i++) {
u8 c = token.string.text[i];
if (c == '"') {
docs = gb_string_appendc(docs, "\"\"");
} else {
docs = gb_string_append_length(docs, &c, 1);
}
}
}
docs = gb_string_appendc(docs, "\"");
}
gb_fprintf(&f,"%.*s,%.*s,%s,%.*s\n", LIT(def->name), LIT(def->default_value_str), docs, LIT(def->pos_str));
}
}
gb_internal void show_defineables(Checker *c) {
for_array(i, c->info.defineables) {
Defineable *def = &c->info.defineables[i];
if (has_ansi_terminal_colours()) {
gb_printf("\x1b[0;90m");
}
printf("%.*s\n", LIT(def->pos_str));
if (def->docs) {
for (Token const &token : def->docs->list) {
gb_printf("%.*s\n", LIT(token.string));
}
}
if (has_ansi_terminal_colours()) {
gb_printf("\x1b[0m");
}
gb_printf("%.*s :: %.*s\n\n", LIT(def->name), LIT(def->default_value_str));
}
}
gb_internal void show_timings(Checker *c, Timings *t) {
Parser *p = c->parser;
isize lines = p->total_line_count;
@@ -1955,6 +2088,15 @@ gb_internal void print_show_help(String const arg0, String const &command) {
print_usage_line(2, "Usage in code:");
print_usage_line(3, "#config(SPAM, default_value)");
print_usage_line(0, "");
print_usage_line(1, "-show-defineables");
print_usage_line(2, "Shows an overview of all the #config/#defined usages in the project.");
print_usage_line(0, "");
print_usage_line(1, "-export-defineables:<filename>");
print_usage_line(2, "Exports an overview of all the #config/#defined usages in CSV format to the given file path.");
print_usage_line(2, "Example: -export-defineables:defineables.csv");
print_usage_line(0, "");
}
if (build) {
@@ -2953,6 +3095,7 @@ int main(int arg_count, char const **arg_ptr) {
defer (destroy_checker(checker));
check_parsed_files(checker);
check_defines(&build_context, checker);
if (any_errors()) {
print_all_errors();
return 1;
@@ -2961,6 +3104,19 @@ int main(int arg_count, char const **arg_ptr) {
print_all_errors();
}
if (build_context.show_defineables || build_context.export_defineables_file != "") {
TEMPORARY_ALLOCATOR_GUARD();
temp_alloc_defineable_strings(checker);
sort_defineables(checker);
if (build_context.show_defineables) {
show_defineables(checker);
}
if (build_context.export_defineables_file != "") {
export_defineables(checker, build_context.export_defineables_file);
}
}
if (build_context.command_kind == Command_strip_semicolon) {
return strip_semicolons(parser);
-3
View File
@@ -20,9 +20,6 @@ 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.create(t.seed)
context.random_generator = rand.default_random_generator(&r)
// Test insertion.
NR_INSERTS :: 32 + 1 // Ensure at least 1 collision.
inserted_map := make(map[int]^avl.Node(int))
@@ -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.create(t.seed)
context.random_generator = rand.default_random_generator(&r)
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)
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -53,8 +53,7 @@ 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)
context.random_generator = rand.default_random_generator(&random_seed)
rand.reset(t.seed)
for len(b) > 0 {
update_size := min(len(b), rand.int_max(8192))
if update_size > 4096 {
+1 -1
View File
@@ -5,7 +5,7 @@ set TEST_ARGS=-fast-tests
set TEST_ARGS=-no-random
set TEST_ARGS=
set OUT_NAME=math_big_test_library.dll
set COMMON=-build-mode:shared -show-timings -no-bounds-check -define:MATH_BIG_EXE=false -vet -strict-style -define:ODIN_TEST_FANCY=false
set COMMON=-build-mode:shared -show-timings -no-bounds-check -define:MATH_BIG_EXE=false -vet -strict-style
echo ---
echo Running core:math/big tests
echo ---
@@ -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.")
}
+3
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"
+63 -5
View File
@@ -11,8 +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)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed)
vals := make([]u64, test_size)
r_idx := make([]int, test_size) // Reverse index
@@ -63,8 +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)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed)
vals := make([]u64, test_size)
r_idx := make([]int, test_size) // Reverse index
@@ -203,7 +201,7 @@ 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
}
@@ -218,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)
}
}
+21 -19
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)
}
}
+135
View File
@@ -0,0 +1,135 @@
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)
}
@test
test_width :: proc(t: ^testing.T) {
{
str := "He\u200dllo"
graphemes, _, width := utf8.grapheme_count(str)
testing.expect_value(t, graphemes, 5)
testing.expect_value(t, width, 5)
}
{
// Note that a zero-width space is still considered a grapheme as far
// as the specification is concerned.
str := "He\u200bllo"
graphemes, _, width := utf8.grapheme_count(str)
testing.expect_value(t, graphemes, 6)
testing.expect_value(t, width, 5)
}
{
str := "\U0001F926\U0001F3FC\u200D\u2642"
graphemes, _, width := utf8.grapheme_count(str)
testing.expect_value(t, graphemes, 1)
testing.expect_value(t, width, 2)
}
{
str := "H̷e̶l̵l̸o̴p̵e̷ ̸w̶o̸r̵l̶d̵!̴"
graphemes, _, width := utf8.grapheme_count(str)
testing.expect_value(t, graphemes, 14)
testing.expect_value(t, width, 14)
}
{
str := "aカ.ヒフ"
graphemes, grapheme_count, _, width := utf8.decode_grapheme_clusters(str)
defer delete(graphemes)
testing.expect_value(t, grapheme_count, 5)
testing.expect_value(t, width, 8)
if grapheme_count == 5 {
testing.expect_value(t, graphemes[0].width, 1)
testing.expect_value(t, graphemes[1].width, 2)
testing.expect_value(t, graphemes[2].width, 1)
testing.expect_value(t, graphemes[3].width, 2)
testing.expect_value(t, graphemes[4].width, 2)
}
}
{
str := "いろはにほへ"
graphemes, _, width := utf8.grapheme_count(str)
testing.expect_value(t, graphemes, 6)
testing.expect_value(t, width, 12)
}
{
str := "舍利弗,是諸法空相,不生不滅,不垢不淨,不增不減。"
graphemes, _, width := utf8.grapheme_count(str)
testing.expect_value(t, graphemes, 25)
testing.expect_value(t, width, 50)
}
}
File diff suppressed because it is too large Load Diff
+13 -26
View File
@@ -16,8 +16,7 @@ map_insert_random_key_value :: proc(t: ^testing.T) {
defer delete(m)
unique_keys := 0
r := rand.create(t.seed + seed_incr)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
for _ in 0..<entries {
k := rand.int63()
v := rand.int63()
@@ -37,8 +36,7 @@ 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)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
num_fails := 0
for _ in 0..<entries {
@@ -68,8 +66,7 @@ map_update_random_key_value :: proc(t: ^testing.T) {
defer delete(m)
unique_keys := 0
r := rand.create(t.seed + seed_incr)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
for _ in 0..<entries {
k := rand.int63()
@@ -92,8 +89,7 @@ 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)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
for _ in 0..<half_entries {
k := rand.int63()
@@ -103,8 +99,7 @@ map_update_random_key_value :: proc(t: ^testing.T) {
}
// Reset randomizer and verify
r = rand.create(t.seed + seed_incr)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
num_fails := 0
for i in 0..<entries {
@@ -135,8 +130,7 @@ map_delete_random_key_value :: proc(t: ^testing.T) {
defer delete(m)
unique_keys := 0
r := rand.create(t.seed + seed_incr)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
for _ in 0..<entries {
k := rand.int63()
@@ -159,8 +153,7 @@ 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)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
for _ in 0..<half_entries {
k := rand.int63()
@@ -170,8 +163,7 @@ map_delete_random_key_value :: proc(t: ^testing.T) {
}
// Reset randomizer and verify
r = rand.create(t.seed + seed_incr)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
num_fails := 0
for i in 0..<entries {
@@ -218,8 +210,7 @@ set_insert_random_key_value :: proc(t: ^testing.T) {
defer delete(m)
unique_keys := 0
r := rand.create(t.seed + seed_incr)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
for _ in 0..<entries {
k := rand.int63()
@@ -238,8 +229,7 @@ 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)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
num_fails := 0
for _ in 0..<entries {
@@ -268,8 +258,7 @@ set_delete_random_key_value :: proc(t: ^testing.T) {
defer delete(m)
unique_keys := 0
r := rand.create(t.seed + seed_incr)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
for _ in 0..<entries {
k := rand.int63()
@@ -291,8 +280,7 @@ 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)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
for _ in 0..<half_entries {
k := rand.int63()
@@ -300,8 +288,7 @@ set_delete_random_key_value :: proc(t: ^testing.T) {
}
// Reset randomizer and verify
r = rand.create(t.seed + seed_incr)
context.random_generator = rand.default_random_generator(&r)
rand.reset(t.seed + seed_incr)
num_fails := 0
for i in 0..<entries {
+24 -22
View File
@@ -3775,39 +3775,41 @@ MESSAGE :: struct {
IInfoQueue_VTable :: struct {
using iunkown_vtable: IUnknown_VTable,
AddApplicationMessage: proc "system" (this: ^IInfoQueue, Severity: MESSAGE_SEVERITY, pDescription: cstring) -> HRESULT,
AddMessage: proc "system" (this: ^IInfoQueue, Category: MESSAGE_CATEGORY, Severity: MESSAGE_SEVERITY, ID: MESSAGE_ID, pDescription: cstring) -> HRESULT,
AddRetrievalFilterEntries: proc "system" (this: ^IInfoQueue, pFilter: ^INFO_QUEUE_FILTER) -> HRESULT,
AddStorageFilterEntries: proc "system" (this: ^IInfoQueue, pFilter: ^INFO_QUEUE_FILTER) -> HRESULT,
ClearRetrievalFilter: proc "system" (this: ^IInfoQueue),
ClearStorageFilter: proc "system" (this: ^IInfoQueue),
SetMessageCountLimit: proc "system" (this: ^IInfoQueue, MessageCountLimit: u64) -> HRESULT,
ClearStoredMessages: proc "system" (this: ^IInfoQueue),
GetBreakOnCategory: proc "system" (this: ^IInfoQueue, Category: MESSAGE_CATEGORY) -> BOOL,
GetBreakOnID: proc "system" (this: ^IInfoQueue, ID: MESSAGE_ID) -> BOOL,
GetBreakOnSeverity: proc "system" (this: ^IInfoQueue, Severity: MESSAGE_SEVERITY) -> BOOL,
GetMessage: proc "system" (this: ^IInfoQueue, MessageIndex: u64, pMessage: ^MESSAGE, pMessageByteLength: ^SIZE_T) -> HRESULT,
GetMessageCountLimit: proc "system" (this: ^IInfoQueue) -> u64,
GetMuteDebugOutput: proc "system" (this: ^IInfoQueue) -> BOOL,
GetNumMessagesAllowedByStorageFilter: proc "system" (this: ^IInfoQueue) -> u64,
GetNumMessagesDeniedByStorageFilter: proc "system" (this: ^IInfoQueue) -> u64,
GetNumMessagesDiscardedByMessageCountLimit: proc "system" (this: ^IInfoQueue) -> u64,
GetNumStoredMessages: proc "system" (this: ^IInfoQueue) -> u64,
GetNumStoredMessagesAllowedByRetrievalFilter: proc "system" (this: ^IInfoQueue) -> u64,
GetRetrievalFilter: proc "system" (this: ^IInfoQueue, pFilter: ^INFO_QUEUE_FILTER, pFilterByteLength: ^SIZE_T) -> HRESULT,
GetRetrievalFilterStackSize: proc "system" (this: ^IInfoQueue) -> u64,
GetNumMessagesDiscardedByMessageCountLimit: proc "system" (this: ^IInfoQueue) -> u64,
GetMessageCountLimit: proc "system" (this: ^IInfoQueue) -> u64,
AddStorageFilterEntries: proc "system" (this: ^IInfoQueue, pFilter: ^INFO_QUEUE_FILTER) -> HRESULT,
GetStorageFilter: proc "system" (this: ^IInfoQueue, pFilter: ^INFO_QUEUE_FILTER, pFilterByteLength: ^SIZE_T) -> HRESULT,
GetStorageFilterStackSize: proc "system" (this: ^IInfoQueue) -> u64,
PopRetrievalFilter: proc "system" (this: ^IInfoQueue),
PopStorageFilter: proc "system" (this: ^IInfoQueue),
PushCopyOfRetrievalFilter: proc "system" (this: ^IInfoQueue) -> HRESULT,
PushCopyOfStorageFilter: proc "system" (this: ^IInfoQueue) -> HRESULT,
PushEmptyRetrievalFilter: proc "system" (this: ^IInfoQueue) -> HRESULT,
ClearStorageFilter: proc "system" (this: ^IInfoQueue),
PushEmptyStorageFilter: proc "system" (this: ^IInfoQueue) -> HRESULT,
PushCopyOfStorageFilter: proc "system" (this: ^IInfoQueue) -> HRESULT,
PushStorageFilter: proc "system" (this: ^IInfoQueue, pFilter: ^INFO_QUEUE_FILTER) -> HRESULT,
PopStorageFilter: proc "system" (this: ^IInfoQueue),
GetStorageFilterStackSize: proc "system" (this: ^IInfoQueue) -> u64,
AddRetrievalFilterEntries: proc "system" (this: ^IInfoQueue, pFilter: ^INFO_QUEUE_FILTER) -> HRESULT,
GetRetrievalFilter: proc "system" (this: ^IInfoQueue, pFilter: ^INFO_QUEUE_FILTER, pFilterByteLength: ^SIZE_T) -> HRESULT,
ClearRetrievalFilter: proc "system" (this: ^IInfoQueue),
PushEmptyRetrievalFilter: proc "system" (this: ^IInfoQueue) -> HRESULT,
PushCopyOfRetrievalFilter: proc "system" (this: ^IInfoQueue) -> HRESULT,
PushRetrievalFilter: proc "system" (this: ^IInfoQueue, pFilter: ^INFO_QUEUE_FILTER) -> HRESULT,
PopRetrievalFilter: proc "system" (this: ^IInfoQueue),
GetRetrievalFilterStackSize: proc "system" (this: ^IInfoQueue) -> u64,
AddMessage: proc "system" (this: ^IInfoQueue, Category: MESSAGE_CATEGORY, Severity: MESSAGE_SEVERITY, ID: MESSAGE_ID, pDescription: cstring) -> HRESULT,
AddApplicationMessage: proc "system" (this: ^IInfoQueue, Severity: MESSAGE_SEVERITY, pDescription: cstring) -> HRESULT,
SetBreakOnCategory: proc "system" (this: ^IInfoQueue, Category: MESSAGE_CATEGORY, bEnable: BOOL) -> HRESULT,
SetBreakOnID: proc "system" (this: ^IInfoQueue, ID: MESSAGE_ID, bEnable: BOOL) -> HRESULT,
SetBreakOnSeverity: proc "system" (this: ^IInfoQueue, Severity: MESSAGE_SEVERITY, bEnable: BOOL) -> HRESULT,
SetMessageCountLimit: proc "system" (this: ^IInfoQueue, MessageCountLimit: u64) -> HRESULT,
SetBreakOnID: proc "system" (this: ^IInfoQueue, ID: MESSAGE_ID, bEnable: BOOL) -> HRESULT,
GetBreakOnCategory: proc "system" (this: ^IInfoQueue, Category: MESSAGE_CATEGORY) -> BOOL,
GetBreakOnSeverity: proc "system" (this: ^IInfoQueue, Severity: MESSAGE_SEVERITY) -> BOOL,
GetBreakOnID: proc "system" (this: ^IInfoQueue, ID: MESSAGE_ID) -> BOOL,
SetMuteDebugOutput: proc "system" (this: ^IInfoQueue, bMute: BOOL),
GetMuteDebugOutput: proc "system" (this: ^IInfoQueue) -> BOOL,
}
MESSAGE_ID :: enum u32 {
+14 -10
View File
@@ -2,14 +2,18 @@
package glfw
// TODO: Native Linux
// Display* glfwGetX11Display(void);
// RRCrtc glfwGetX11Adapter(GLFWmonitor* monitor);
// RROutput glfwGetX11Monitor(GLFWmonitor* monitor);
// Window glfwGetX11Window(GLFWwindow* window);
// void glfwSetX11SelectionString(const char* string);
// const char* glfwGetX11SelectionString(void);
import "vendor:x11/xlib"
// struct wl_display* glfwGetWaylandDisplay(void);
// struct wl_output* glfwGetWaylandMonitor(GLFWmonitor* monitor);
// struct wl_surface* glfwGetWaylandWindow(GLFWwindow* window);
@(default_calling_convention="c", link_prefix="glfw")
foreign {
GetX11Display :: proc() -> ^xlib.Display ---
GetX11Window :: proc(window: WindowHandle) -> xlib.Window ---
GetX11Adapter :: proc(monitor: MonitorHandle) -> xlib.RRCrtc ---
GetX11Monitor :: proc(monitor: MonitorHandle) -> xlib.RROutput ---
SetX11SelectionString :: proc(string: cstring) ---
GetX11SelectionString :: proc() -> cstring ---
GetWaylandDisplay :: proc() -> rawptr /* struct wl_display* */ ---
GetWaylandWindow :: proc(window: WindowHandle) -> rawptr /* struct wl_surface* */ ---
GetWaylandMonitor :: proc(monitor: MonitorHandle) -> rawptr /* struct wl_output* */ ---
}
+37 -7
View File
@@ -96,6 +96,10 @@ class WasmMemoryInterface {
};
loadPtr(addr) { return this.loadU32(addr); }
loadB32(addr) {
return this.loadU32(addr) != 0;
}
loadBytes(ptr, len) {
return new Uint8Array(this.memory.buffer, ptr, Number(len));
}
@@ -104,6 +108,16 @@ class WasmMemoryInterface {
const bytes = this.loadBytes(ptr, Number(len));
return new TextDecoder().decode(bytes);
}
loadCstring(ptr) {
const start = this.loadPtr(ptr);
if (start == 0) {
return null;
}
let len = 0;
for (; this.mem.getUint8(start+len) != 0; len += 1) {}
return this.loadString(start, len);
}
storeU8(addr, value) { this.mem.setUint8 (addr, value); }
storeI8(addr, value) { this.mem.setInt8 (addr, value); }
@@ -1245,7 +1259,7 @@ class WebGLInterface {
};
function odinSetupDefaultImports(wasmMemoryInterface, consoleElement) {
function odinSetupDefaultImports(wasmMemoryInterface, consoleElement, memory) {
const MAX_INFO_CONSOLE_LINES = 512;
let infoConsoleLines = new Array();
let currentLine = {};
@@ -1366,8 +1380,15 @@ function odinSetupDefaultImports(wasmMemoryInterface, consoleElement) {
let event_temp_data = {};
let webglContext = new WebGLInterface(wasmMemoryInterface);
const env = {};
if (memory) {
env.memory = memory;
}
return {
"env": {},
env,
"odin_env": {
write: (fd, ptr, len) => {
const str = wasmMemoryInterface.loadString(ptr, len);
@@ -1720,13 +1741,16 @@ function odinSetupDefaultImports(wasmMemoryInterface, consoleElement) {
* @param {string} wasmPath - Path to the WASM module to run
* @param {?HTMLPreElement} consoleElement - Optional console/pre element to append output to, in addition to the console
* @param {any} extraForeignImports - Imports, in addition to the default runtime to provide the module
* @param {?WasmMemoryInterface} wasmMemoryInterface - Optional memory to use instead of the defaults
* @param {?int} intSize - Size (in bytes) of the integer type, should be 4 on `js_wasm32` and 8 on `js_wasm64p32`
*/
async function runWasm(wasmPath, consoleElement, extraForeignImports, intSize = 4) {
const wasmMemoryInterface = new WasmMemoryInterface();
async function runWasm(wasmPath, consoleElement, extraForeignImports, wasmMemoryInterface, intSize = 4) {
if (!wasmMemoryInterface) {
wasmMemoryInterface = new WasmMemoryInterface();
}
wasmMemoryInterface.setIntSize(intSize);
let imports = odinSetupDefaultImports(wasmMemoryInterface, consoleElement);
let imports = odinSetupDefaultImports(wasmMemoryInterface, consoleElement, wasmMemoryInterface.memory);
let exports = {};
if (extraForeignImports !== undefined) {
@@ -1741,11 +1765,17 @@ async function runWasm(wasmPath, consoleElement, extraForeignImports, intSize =
const wasm = await WebAssembly.instantiate(file, imports);
exports = wasm.instance.exports;
wasmMemoryInterface.setExports(exports);
wasmMemoryInterface.setMemory(exports.memory);
if (exports.memory) {
if (wasmMemoryInterface.memory) {
console.warn("WASM module exports memory, but `runWasm` was given an interface with existing memory too");
}
wasmMemoryInterface.setMemory(exports.memory);
}
exports._start();
// Define a `@export step :: proc(dt: f32) -> (continue: bool) {`
// Define a `@export step :: proc(dt: f32) -> (keep_going: bool) {`
// in your app and it will get called every frame.
// return `false` to stop the execution of the module.
if (exports.step) {
+9
View File
@@ -0,0 +1,9 @@
lib/*
!lib/.gitkeep
example/web/triangle.wasm
example/web/wgpu.js
example/web/runtime.js
example/example
example/example.exe
example/triangle
example/triangle.exe
+48
View File
@@ -0,0 +1,48 @@
# WGPU
A cross-platform (and WASM) GPU API.
WASM support is achieved by providing wrappers around the browser native WebGPU API
that are called instead of the [wgpu-native](https://github.com/gfx-rs/wgpu-native) library,
the wgpu-native library provides support for all other targets.
Have a look at the `example/` directory for the rendering of a basic triangle.
## Getting the wgpu-native libraries
For native support (not the browser), some libraries are required. Fortunately this is
extremely easy, just download them from the [releases on GitHub](https://github.com/gfx-rs/wgpu-native/releases/tag/v0.19.4.1),
the bindings are for v0.19.4.1 at the moment.
These are expected in the `lib` folder under the same name as they are released (just unzipped).
By default it will look for a static release version (`wgpu-OS-ARCH-release.a|lib`),
you can set `-define:WGPU_DEBUG=true` for it to look for a debug version,
and use `-define:WGPU_SHARED=true` to look for the shared libraries.
## WASM
For WASM, the module has to be built with a function table to enable callbacks.
You can do so using `-extra-linker-flags:"--export-table"`.
Being able to allocate is also required (for some auxiliary APIs but also for mapping/unmapping buffers).
You can set the context that is used for allocations by setting the global variable `wpgu.g_context`.
It will default to the `runtime.default_context`.
Again, have a look at the `example/` and how it is set up, doing the `--import-memory` and the likes
is not strictly necessary but allows your app more memory than the minimal default.
The bindings work on both `-target:js_wasm32` and `-target:js_wasm64p32`.
## GLFW Glue
There is an inner package `glfwglue` that can be used to glue together WGPU and GLFW.
It exports one procedure `GetSurface(wgpu.Instance, glfw.WindowHandle) -> glfw.Surface`.
The procedure will call the needed target specific procedures and return a surface configured
for the given window.
To support Wayland on Linux, you need to have GLFW compiled to support it, and use
`-define:WGPU_GFLW_GLUE_SUPPORT_WAYLAND=true` to enable the package to check for Wayland.
Do note that wgpu does not require GLFW, you can use native windows or another windowing library too.
For that you can take inspiration from `glfwglue` on glueing them together.
+17
View File
@@ -0,0 +1,17 @@
FILES := $(wildcard *)
# NOTE: changing this requires changing the same values in the `web/index.html`.
INITIAL_MEMORY_PAGES := 2000
MAX_MEMORY_PAGES := 65536
PAGE_SIZE := 65536
INITIAL_MEMORY_BYTES := $(shell expr $(INITIAL_MEMORY_PAGES) \* $(PAGE_SIZE))
MAX_MEMORY_BYTES := $(shell expr $(MAX_MEMORY_PAGES) \* $(PAGE_SIZE))
web/triangle.wasm: $(FILES) ../wgpu.js ../../wasm/js/runtime.js
odin build . \
-target:js_wasm32 -out:web/triangle.wasm -o:size \
-extra-linker-flags:"--export-table --import-memory --initial-memory=$(INITIAL_MEMORY_BYTES) --max-memory=$(MAX_MEMORY_BYTES)"
cp ../wgpu.js web/wgpu.js
cp ../../wasm/js/runtime.js web/runtime.js
+12
View File
@@ -0,0 +1,12 @@
REM NOTE: changing this requires changing the same values in the `web/index.html`.
set INITIAL_MEMORY_PAGES=2000
set MAX_MEMORY_PAGES=65536
set PAGE_SIZE=65536
set /a INITIAL_MEMORY_BYTES=%INITIAL_MEMORY_PAGES% * %PAGE_SIZE%
set /a MAX_MEMORY_BYTES=%MAX_MEMORY_PAGES% * %PAGE_SIZE%
call odin.exe build . -target:js_wasm32 -out:web/triangle.wasm -o:size -extra-linker-flags:"--export-table --import-memory --initial-memory=%INITIAL_MEMORY_BYTES% --max-memory=%MAX_MEMORY_BYTES%"
copy "..\wgpu.js" "web\wgpu.js"
copy "..\..\wasm\js\runtime.js" "web\runtime.js"
+187
View File
@@ -0,0 +1,187 @@
package vendor_wgpu_example_triangle
import "base:runtime"
import "core:fmt"
import "vendor:wgpu"
State :: struct {
ctx: runtime.Context,
os: OS,
instance: wgpu.Instance,
surface: wgpu.Surface,
adapter: wgpu.Adapter,
device: wgpu.Device,
config: wgpu.SurfaceConfiguration,
queue: wgpu.Queue,
module: wgpu.ShaderModule,
pipeline_layout: wgpu.PipelineLayout,
pipeline: wgpu.RenderPipeline,
}
@(private="file")
state: State
main :: proc() {
state.ctx = context
os_init(&state.os)
state.instance = wgpu.CreateInstance(nil)
if state.instance == nil {
panic("WebGPU is not supported")
}
state.surface = os_get_surface(&state.os, state.instance)
wgpu.InstanceRequestAdapter(state.instance, &{ compatibleSurface = state.surface }, on_adapter, nil)
on_adapter :: proc "c" (status: wgpu.RequestAdapterStatus, adapter: wgpu.Adapter, message: cstring, userdata: rawptr) {
context = state.ctx
if status != .Success || adapter == nil {
fmt.panicf("request adapter failure: [%v] %s", status, message)
}
state.adapter = adapter
wgpu.AdapterRequestDevice(adapter, nil, on_device)
}
on_device :: proc "c" (status: wgpu.RequestDeviceStatus, device: wgpu.Device, message: cstring, userdata: rawptr) {
context = state.ctx
if status != .Success || device == nil {
fmt.panicf("request device failure: [%v] %s", status, message)
}
state.device = device
width, height := os_get_render_bounds(&state.os)
state.config = wgpu.SurfaceConfiguration {
device = state.device,
usage = { .RenderAttachment },
format = .BGRA8Unorm,
width = width,
height = height,
presentMode = .Fifo,
alphaMode = .Opaque,
}
wgpu.SurfaceConfigure(state.surface, &state.config)
state.queue = wgpu.DeviceGetQueue(state.device)
shader :: `
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
let x = f32(i32(in_vertex_index) - 1);
let y = f32(i32(in_vertex_index & 1u) * 2 - 1);
return vec4<f32>(x, y, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}`
state.module = wgpu.DeviceCreateShaderModule(state.device, &{
nextInChain = &wgpu.ShaderModuleWGSLDescriptor{
sType = .ShaderModuleWGSLDescriptor,
code = shader,
},
})
state.pipeline_layout = wgpu.DeviceCreatePipelineLayout(state.device, &{})
state.pipeline = wgpu.DeviceCreateRenderPipeline(state.device, &{
layout = state.pipeline_layout,
vertex = {
module = state.module,
entryPoint = "vs_main",
},
fragment = &{
module = state.module,
entryPoint = "fs_main",
targetCount = 1,
targets = &wgpu.ColorTargetState{
format = .BGRA8Unorm,
writeMask = wgpu.ColorWriteMaskFlags_All,
},
},
primitive = {
topology = .TriangleList,
},
multisample = {
count = 1,
mask = 0xFFFFFFFF,
},
})
os_run(&state.os)
}
}
resize :: proc "c" () {
context = state.ctx
state.config.width, state.config.height = os_get_render_bounds(&state.os)
wgpu.SurfaceConfigure(state.surface, &state.config)
}
frame :: proc "c" (dt: f32) {
context = state.ctx
surface_texture := wgpu.SurfaceGetCurrentTexture(state.surface)
switch surface_texture.status {
case .Success:
// All good, could check for `surface_texture.suboptimal` here.
case .Timeout, .Outdated, .Lost:
// Skip this frame, and re-configure surface.
if surface_texture.texture != nil {
wgpu.TextureRelease(surface_texture.texture)
}
resize()
return
case .OutOfMemory, .DeviceLost:
// Fatal error
fmt.panicf("[triangle] get_current_texture status=%v", surface_texture.status)
}
defer wgpu.TextureRelease(surface_texture.texture)
frame := wgpu.TextureCreateView(surface_texture.texture, nil)
defer wgpu.TextureViewRelease(frame)
command_encoder := wgpu.DeviceCreateCommandEncoder(state.device, nil)
defer wgpu.CommandEncoderRelease(command_encoder)
render_pass_encoder := wgpu.CommandEncoderBeginRenderPass(
command_encoder, &{
colorAttachmentCount = 1,
colorAttachments = &wgpu.RenderPassColorAttachment{
view = frame,
loadOp = .Clear,
storeOp = .Store,
clearValue = { r = 0, g = 1, b = 0, a = 1 },
},
},
)
defer wgpu.RenderPassEncoderRelease(render_pass_encoder)
wgpu.RenderPassEncoderSetPipeline(render_pass_encoder, state.pipeline)
wgpu.RenderPassEncoderDraw(render_pass_encoder, vertexCount=3, instanceCount=1, firstVertex=0, firstInstance=0)
wgpu.RenderPassEncoderEnd(render_pass_encoder)
command_buffer := wgpu.CommandEncoderFinish(command_encoder, nil)
defer wgpu.CommandBufferRelease(command_buffer)
wgpu.QueueSubmit(state.queue, { command_buffer })
wgpu.SurfacePresent(state.surface)
}
finish :: proc() {
wgpu.RenderPipelineRelease(state.pipeline)
wgpu.PipelineLayoutRelease(state.pipeline_layout)
wgpu.ShaderModuleRelease(state.module)
wgpu.QueueRelease(state.queue)
wgpu.DeviceRelease(state.device)
wgpu.AdapterRelease(state.adapter)
wgpu.SurfaceRelease(state.surface)
wgpu.InstanceRelease(state.instance)
}
+55
View File
@@ -0,0 +1,55 @@
//+build !js
package vendor_wgpu_example_triangle
import "core:time"
import "vendor:glfw"
import "vendor:wgpu"
import "vendor:wgpu/glfwglue"
OS :: struct {
window: glfw.WindowHandle,
}
os_init :: proc(os: ^OS) {
if !glfw.Init() {
panic("[glfw] init failure")
}
glfw.WindowHint(glfw.CLIENT_API, glfw.NO_API)
os.window = glfw.CreateWindow(960, 540, "WGPU Native Triangle", nil, nil)
glfw.SetFramebufferSizeCallback(os.window, size_callback)
}
os_run :: proc(os: ^OS) {
dt: f32
for !glfw.WindowShouldClose(os.window) {
start := time.tick_now()
glfw.PollEvents()
frame(dt)
dt = f32(time.duration_seconds(time.tick_since(start)))
}
finish()
glfw.DestroyWindow(os.window)
glfw.Terminate()
}
os_get_render_bounds :: proc(os: ^OS) -> (width, height: u32) {
iw, ih := glfw.GetWindowSize(os.window)
return u32(iw), u32(ih)
}
os_get_surface :: proc(os: ^OS, instance: wgpu.Instance) -> wgpu.Surface {
return glfwglue.GetSurface(instance, os.window)
}
@(private="file")
size_callback :: proc "c" (window: glfw.WindowHandle, width, height: i32) {
resize()
}
+60
View File
@@ -0,0 +1,60 @@
package vendor_wgpu_example_triangle
import "vendor:wgpu"
import "vendor:wasm/js"
OS :: struct {
initialized: bool,
}
@(private="file")
g_os: ^OS
os_init :: proc(os: ^OS) {
g_os = os
assert(js.add_window_event_listener(.Resize, nil, size_callback))
}
// NOTE: frame loop is done by the runtime.js repeatedly calling `step`.
os_run :: proc(os: ^OS) {
os.initialized = true
}
os_get_render_bounds :: proc(os: ^OS) -> (width, height: u32) {
rect := js.get_bounding_client_rect("body")
return u32(rect.width), u32(rect.height)
}
os_get_surface :: proc(os: ^OS, instance: wgpu.Instance) -> wgpu.Surface {
return wgpu.InstanceCreateSurface(
instance,
&wgpu.SurfaceDescriptor{
nextInChain = &wgpu.SurfaceDescriptorFromCanvasHTMLSelector{
sType = .SurfaceDescriptorFromCanvasHTMLSelector,
selector = "#wgpu-canvas",
},
},
)
}
@(private="file", export)
step :: proc(dt: f32) -> bool {
if !g_os.initialized {
return true
}
frame(dt)
return true
}
@(private="file", fini)
os_fini :: proc() {
js.remove_window_event_listener(.Resize, nil, size_callback)
finish()
}
@(private="file")
size_callback :: proc(e: js.Event) {
resize()
}
+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en" style="height: 100%;">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WGPU WASM Triangle</title>
</head>
<body id="body" style="height: 100%; padding: 0; margin: 0; overflow: hidden;">
<canvas id="wgpu-canvas"></canvas>
<script type="text/javascript" src="runtime.js"></script>
<script type="text/javascript" src="wgpu.js"></script>
<script type="text/javascript">
const mem = new WebAssembly.Memory({ initial: 2000, maximum: 65536, shared: false });
const memInterface = new odin.WasmMemoryInterface();
memInterface.setMemory(mem);
const wgpuInterface = new odin.WebGPUInterface(memInterface);
odin.runWasm("triangle.wasm", null, { wgpu: wgpuInterface.getInterface() }, memInterface, /*intSize=8*/);
</script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
//+build !linux
//+build !windows
//+build !darwin
package wgpu_glfw_glue
#panic("package wgpu/glfwglue is not supported on the current target")
+23
View File
@@ -0,0 +1,23 @@
package wgpu_glfw_glue
import "vendor:glfw"
import "vendor:wgpu"
import CA "vendor:darwin/QuartzCore"
GetSurface :: proc(instance: wgpu.Instance, window: glfw.WindowHandle) -> wgpu.Surface {
ns_window := glfw.GetCocoaWindow(window)
ns_window->contentView()->setWantsLayer(true)
metal_layer := CA.MetalLayer_layer()
ns_window->contentView()->setLayer(metal_layer)
return wgpu.InstanceCreateSurface(
instance,
&wgpu.SurfaceDescriptor{
nextInChain = &wgpu.SurfaceDescriptorFromMetalLayer{
chain = wgpu.ChainedStruct{
sType = .SurfaceDescriptorFromMetalLayer,
},
layer = rawptr(metal_layer),
},
},
)
}
+43
View File
@@ -0,0 +1,43 @@
package wgpu_glfw_glue
import "vendor:glfw"
import "vendor:wgpu"
// GLFW needs to be compiled with wayland support for this to work.
SUPPORT_WAYLAND :: #config(WGPU_GFLW_GLUE_SUPPORT_WAYLAND, false)
GetSurface :: proc(instance: wgpu.Instance, window: glfw.WindowHandle) -> wgpu.Surface {
when SUPPORT_WAYLAND {
if glfw.GetPlatform() == glfw.PLATFORM_WAYLAND {
display := glfw.GetWaylandDisplay()
surface := glfw.GetWaylandWindow(window)
return wgpu.InstanceCreateSurface(
instance,
&wgpu.SurfaceDescriptor{
nextInChain = &wgpu.SurfaceDescriptorFromWaylandSurface{
chain = {
sType = .SurfaceDescriptorFromWaylandSurface,
},
display = display,
surface = surface,
},
},
)
}
}
display := glfw.GetX11Display()
window := glfw.GetX11Window(window)
return wgpu.InstanceCreateSurface(
instance,
&wgpu.SurfaceDescriptor{
nextInChain = &wgpu.SurfaceDescriptorFromXlibWindow{
chain = {
sType = .SurfaceDescriptorFromXlibWindow,
},
display = display,
window = u64(window),
},
},
)
}
+23
View File
@@ -0,0 +1,23 @@
package wgpu_glfw_glue
import win "core:sys/windows"
import "vendor:glfw"
import "vendor:wgpu"
GetSurface :: proc(instance: wgpu.Instance, window: glfw.WindowHandle) -> wgpu.Surface {
hwnd := glfw.GetWin32Window(window)
hinstance := win.GetModuleHandleW(nil)
return wgpu.InstanceCreateSurface(
instance,
&wgpu.SurfaceDescriptor{
nextInChain = &wgpu.SurfaceDescriptorFromWindowsHWND{
chain = wgpu.ChainedStruct{
sType = .SurfaceDescriptorFromWindowsHWND,
},
hinstance = rawptr(hinstance),
hwnd = rawptr(hwnd),
},
},
)
}
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2916
View File
File diff suppressed because it is too large Load Diff
+1636
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
package wgpu
import "base:runtime"
g_context: runtime.Context
@(private="file", init)
wgpu_init_allocator :: proc() {
if g_context.allocator.procedure == nil {
g_context = runtime.default_context()
}
}
@(private="file", export)
wgpu_alloc :: proc "contextless" (size: i32) -> [^]byte {
context = g_context
bytes, err := runtime.mem_alloc(int(size), 16)
assert(err == nil, "wgpu_alloc failed")
return raw_data(bytes)
}
@(private="file", export)
wgpu_free :: proc "contextless" (ptr: rawptr) {
context = g_context
assert(free(ptr) == nil, "wgpu_free failed")
}
+75
View File
@@ -0,0 +1,75 @@
//+build !js
package wgpu
BINDINGS_VERSION :: [4]u8{0, 19, 4, 1}
BINDINGS_VERSION_STRING :: "0.19.4.1"
@(private="file", init)
wgpu_native_version_check :: proc() {
v := (transmute([4]u8)GetVersion()).wzyx
if v != BINDINGS_VERSION {
buf: [1024]byte
n := copy(buf[:], "wgpu-native version mismatch: ")
n += copy(buf[n:], "bindings are for version ")
n += copy(buf[n:], BINDINGS_VERSION_STRING)
n += copy(buf[n:], ", but a different version is linked")
panic(string(buf[:n]))
}
}
@(link_prefix="wgpu")
foreign {
@(link_name="wgpuGenerateReport")
RawGenerateReport :: proc(instance: Instance, report: ^GlobalReport) ---
@(link_name="wgpuInstanceEnumerateAdapters")
RawInstanceEnumerateAdapters :: proc(instance: Instance, /* NULLABLE */ options: /* const */ ^InstanceEnumerateAdapterOptions, adapters: [^]Adapter) -> uint ---
@(link_name="wgpuQueueSubmitForIndex")
RawQueueSubmitForIndex :: proc(queue: Queue, commandCount: uint, commands: [^]CommandBuffer) -> SubmissionIndex ---
// Returns true if the queue is empty, or false if there are more queue submissions still in flight.
@(link_name="wgpuDevicePoll")
RawDevicePoll :: proc(device: Device, wait: b32, /* NULLABLE */ wrappedSubmissionIndex: /* const */ ^WrappedSubmissionIndex) -> b32 ---
SetLogCallback :: proc "odin" (callback: LogCallback) ---
SetLogLevel :: proc(level: LogLevel) ---
GetVersion :: proc() -> u32 ---
RenderPassEncoderSetPushConstants :: proc(encoder: RenderPassEncoder, stages: ShaderStageFlags, offset: u32, sizeBytes: u32, data: cstring) ---
RenderPassEncoderMultiDrawIndirect :: proc(encoder: RenderPassEncoder, buffer: Buffer, offset: u64, count: u32) ---
RenderPassEncoderMultiDrawIndexedIndirect :: proc(encoder: RenderPassEncoder, buffer: Buffer, offset: u64, count: u32) ---
RenderPassEncoderMultiDrawIndirectCount :: proc(encoder: RenderPassEncoder, buffer: Buffer, offset: u64, count_buffer: Buffer, count_buffer_offset: u64, max_count: u32) ---
RenderPassEncoderMultiDrawIndexedIndirectCount :: proc(encoder: RenderPassEncoder, buffer: Buffer, offset: u64, count_buffer: Buffer, count_buffer_offset: u64, max_count: u32) ---
ComputePassEncoderBeginPipelineStatisticsQuery :: proc(computePassEncoder: ComputePassEncoder, querySet: QuerySet, queryIndex: u32) ---
ComputePassEncoderEndPipelineStatisticsQuery :: proc(computePassEncoder: ComputePassEncoder) ---
RenderPassEncoderBeginPipelineStatisticsQuery :: proc(renderPassEncoder: RenderPassEncoder, querySet: QuerySet, queryIndex: u32) ---
RenderPassEncoderEndPipelineStatisticsQuery :: proc(renderPassEncoder: RenderPassEncoder) ---
}
GenerateReport :: proc(instance: Instance) -> (report: GlobalReport) {
RawGenerateReport(instance, &report)
return
}
InstanceEnumerateAdapters :: proc(instance: Instance, options: ^InstanceEnumerateAdapterOptions = nil, allocator := context.allocator) -> (adapters: []Adapter) {
count := RawInstanceEnumerateAdapters(instance, options, nil)
adapters = make([]Adapter, count, allocator)
RawInstanceEnumerateAdapters(instance, options, raw_data(adapters))
return
}
QueueSubmitForIndex :: proc(queue: Queue, commands: []CommandBuffer) -> SubmissionIndex {
return RawQueueSubmitForIndex(queue, len(commands), raw_data(commands))
}
DevicePoll :: proc(device: Device, wait: b32) -> (wrappedSubmissionIndex: WrappedSubmissionIndex, ok: bool) {
ok = bool(RawDevicePoll(device, wait, &wrappedSubmissionIndex))
return
}
+212
View File
@@ -0,0 +1,212 @@
package wgpu
import "base:runtime"
LogLevel :: enum i32 {
Off,
Error,
Warn,
Info,
Debug,
Trace,
}
InstanceBackend :: enum i32 {
Vulkan,
GL,
Metal,
DX12,
DX11,
BrowserWebGPU,
}
InstanceBackendFlags :: bit_set[InstanceBackend; Flags]
InstanceBackendFlags_All :: InstanceBackendFlags{}
InstanceBackendFlags_Primary :: InstanceBackendFlags{ .Vulkan, .Metal, .DX12, .BrowserWebGPU }
InstanceBackendFlags_Secondary :: InstanceBackendFlags{ .GL, .DX11 }
InstanceFlag :: enum i32 {
Debug,
Validation,
DiscardHalLabels,
}
InstanceFlags :: bit_set[InstanceFlag; Flags]
InstanceFlags_Default :: InstanceFlags{}
Dx12Compiler :: enum i32 {
Undefined,
Fxc,
Dxc,
}
Gles3MinorVersion :: enum i32 {
Automatic,
Version0,
Version1,
Version2,
}
PipelineStatisticName :: enum i32 {
VertexShaderInvocations,
ClipperInvocations,
ClipperPrimitivesOut,
FragmentShaderInvocations,
ComputeShaderInvocations,
}
InstanceExtras :: struct {
using chain: ChainedStruct,
backends: InstanceBackendFlags,
flags: InstanceFlags,
dx12ShaderCompiler: Dx12Compiler,
gles3MinorVersion: Gles3MinorVersion,
dxilPath: cstring,
dxcPath: cstring,
}
DeviceExtras :: struct {
using chain: ChainedStruct,
tracePath: cstring,
}
NativeLimits :: struct {
maxPushConstantSize: u32,
maxNonSamplerBindings: u32,
}
RequiredLimitsExtras :: struct {
using chain: ChainedStruct,
limits: NativeLimits,
}
SupportedLimitsExtras :: struct {
using chain: ChainedStruct,
limits: NativeLimits,
}
PushConstantRange :: struct {
stages: ShaderStageFlags,
start: u32,
end: u32,
}
PipelineLayoutExtras :: struct {
using chain: ChainedStruct,
pushConstantRangeCount: uint,
pushConstantRanges: [^]PushConstantRange `fmt:"v,pushConstantRangeCount"`,
}
SubmissionIndex :: distinct u64
WrappedSubmissionIndex :: struct {
queue: Queue,
submissionIndex: SubmissionIndex,
}
ShaderDefine :: struct {
name: cstring,
value: cstring,
}
ShaderModuleGLSLDescriptor :: struct {
using chain: ChainedStruct,
stage: ShaderStage,
code: cstring,
defineCount: uint,
defines: [^]ShaderDefine `fmt:"v,defineCount"`,
}
RegistryReport :: struct {
numAllocated: uint,
numKeptFromUser: uint,
numReleasedFromUser: uint,
numErrors: uint,
elementSize: uint,
}
HubReport :: struct {
adapters: RegistryReport,
devices: RegistryReport,
queues: RegistryReport,
pipelineLayouts: RegistryReport,
shaderModules: RegistryReport,
bindGroupLayouts: RegistryReport,
bindGroups: RegistryReport,
commandBuffers: RegistryReport,
renderBundles: RegistryReport,
renderPipelines: RegistryReport,
computePipelines: RegistryReport,
querySets: RegistryReport,
buffers: RegistryReport,
textures: RegistryReport,
textureViews: RegistryReport,
samplers: RegistryReport,
}
GlobalReport :: struct {
surfaces: RegistryReport,
backendType: BackendType,
vulkan: HubReport,
metal: HubReport,
dx12: HubReport,
gl: HubReport,
}
InstanceEnumerateAdapterOptions :: struct {
nextInChain: ^ChainedStruct,
backends: InstanceBackendFlags,
}
BindGroupEntryExtras :: struct {
using chain: ChainedStruct,
buffers: [^]Buffer `fmt:"v,bufferCount"`,
bufferCount: uint,
samplers: [^]Sampler `fmt:"v,samplerCount"`,
samplerCount: uint,
textureViews: [^]TextureView `fmt:"v,textureViewCount"`,
textureViewCount: uint,
}
BindGroupLayoutEntryExtras :: struct {
using chain: ChainedStruct,
count: u32,
}
QuerySetDescriptorExtras :: struct {
using chain: ChainedStruct,
pipelineStatistics: [^]PipelineStatisticName `fmt:"v,pipelineStatisticCount"`,
pipelineStatisticCount: uint,
}
SurfaceConfigurationExtras :: struct {
using chain: ChainedStruct,
desiredMaximumFrameLatency: i32,
}
LogCallback :: #type proc "odin" (level: LogLevel, message: cstring)
// Wrappers
ConvertOdinToWGPULogLevel :: proc(level: runtime.Logger_Level) -> LogLevel {
switch {
case level < .Debug: return .Trace
case level < .Info: return .Debug
case level < .Warning: return .Info
case level < .Error: return .Warn
case: return .Error
}
}
ConvertWGPUToOdinLogLevel :: proc(level: LogLevel) -> runtime.Logger_Level {
switch level {
case .Off, .Trace, .Debug: return .Debug
case .Info: return .Info
case .Warn: return .Warning
case .Error: return .Error
case: return .Error
}
}
ConvertLogLevel :: proc {
ConvertOdinToWGPULogLevel,
ConvertWGPUToOdinLogLevel,
}
+3
View File
@@ -24,6 +24,9 @@ Cursor :: XID
Colormap :: XID
GContext :: XID
RRCrtc :: XID
RROutput :: XID
KeyCode :: u8
/* ---- X11/Xlib.h ---------------------------------------------------------*/