Browse Source

Merge remote-tracking branch 'origin' into wsapoll

Jon Lipstate 2 years ago
parent
commit
2550918f27

+ 7 - 0
.github/workflows/ci.yml

@@ -163,6 +163,13 @@ jobs:
           cd tests\internal
           call build.bat
         timeout-minutes: 10
+      - name: Odin documentation tests
+        shell: cmd
+        run: |
+          call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat
+          cd tests\documentation
+          call build.bat
+        timeout-minutes: 10
       - name: core:math/big tests
         shell: cmd
         run: |

+ 2 - 0
core/crypto/util/util.odin

@@ -11,6 +11,8 @@ package util
 */
 
 import "core:mem"
+// Keep vet happy
+_ :: mem
 
 // @note(bp): this can replace the other two
 cast_slice :: #force_inline proc "contextless" ($D: typeid/[]$DE, src: $S/[]$SE) -> D {

+ 5 - 4
core/strings/ascii_set.odin

@@ -12,10 +12,10 @@ Ascii_Set :: distinct [8]u32
 /*
 Creates an Ascii_Set with unique characters from the input string.
 
-**Inputs**  
+Inputs:
 - chars: A string containing characters to include in the Ascii_Set.
 
-**Returns**  
+Returns:
 - as: An Ascii_Set with unique characters from the input string.
 - ok: false if any character in the input string is not a valid ASCII character.
 */
@@ -33,11 +33,12 @@ ascii_set_make :: proc(chars: string) -> (as: Ascii_Set, ok: bool) #no_bounds_ch
 /*
 Determines if a given char is contained within an Ascii_Set.
 
-**Inputs**  
+Inputs:
 - as: The Ascii_Set to search.
 - c: The char to check for in the Ascii_Set.
 
-**Returns**  A boolean indicating if the byte is contained in the Ascii_Set (true) or not (false).
+Returns:
+A boolean indicating if the byte is contained in the Ascii_Set (true) or not (false).
 */
 ascii_set_contains :: proc(as: Ascii_Set, c: byte) -> bool #no_bounds_check {
 	return as[c>>5] & (1<<(c&31)) != 0

+ 133 - 91
core/strings/builder.odin

@@ -7,10 +7,11 @@ import "core:io"
 /*
 Type definition for a procedure that flushes a Builder
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 
-**Returns**  A boolean indicating whether the Builder should be reset
+Returns:
+A boolean indicating whether the Builder should be reset
 */
 Builder_Flush_Proc :: #type proc(b: ^Builder) -> (do_reset: bool)
 /*
@@ -26,10 +27,11 @@ Produces a Builder with a default length of 0 and cap of 16
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - allocator: (default is context.allocator)
 
-**Returns**  A new Builder
+Returns:
+A new Builder
 */
 builder_make_none :: proc(allocator := context.allocator) -> Builder {
 	return Builder{buf=make([dynamic]byte, allocator)}
@@ -39,11 +41,12 @@ Produces a Builder with a specified length and cap of max(16,len) byte buffer
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - len: The desired length of the Builder's buffer
 - allocator: (default is context.allocator)
 
-**Returns**  A new Builder
+Returns:
+A new Builder
 */
 builder_make_len :: proc(len: int, allocator := context.allocator) -> Builder {
 	return Builder{buf=make([dynamic]byte, len, allocator)}
@@ -53,12 +56,13 @@ Produces a Builder with a specified length and cap
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - len: The desired length of the Builder's buffer
 - cap: The desired capacity of the Builder's buffer, cap is max(cap, len)
 - allocator: (default is context.allocator)
 
-**Returns**  A new Builder
+Returns:
+A new Builder
 */
 builder_make_len_cap :: proc(len, cap: int, allocator := context.allocator) -> Builder {
 	return Builder{buf=make([dynamic]byte, len, cap, allocator)}
@@ -75,11 +79,12 @@ It replaces the existing `buf`
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - allocator: (default is context.allocator)
 
-**Returns**  initialized ^Builder
+Returns:
+initialized ^Builder
 */
 builder_init_none :: proc(b: ^Builder, allocator := context.allocator) -> ^Builder {
 	b.buf = make([dynamic]byte, allocator)
@@ -91,12 +96,13 @@ It replaces the existing `buf`
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - len: The desired length of the Builder's buffer
 - allocator: (default is context.allocator)
 
-**Returns**  Initialized ^Builder
+Returns:
+Initialized ^Builder
 */
 builder_init_len :: proc(b: ^Builder, len: int, allocator := context.allocator) -> ^Builder {
 	b.buf = make([dynamic]byte, len, allocator)
@@ -106,13 +112,14 @@ builder_init_len :: proc(b: ^Builder, len: int, allocator := context.allocator)
 Initializes a Builder with a specified length and cap
 It replaces the existing `buf`
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - len: The desired length of the Builder's buffer
 - cap: The desired capacity of the Builder's buffer, actual max(len,cap)
 - allocator: (default is context.allocator)
 
-**Returns**  A pointer to the initialized Builder
+Returns:
+A pointer to the initialized Builder
 */
 builder_init_len_cap :: proc(b: ^Builder, len, cap: int, allocator := context.allocator) -> ^Builder {
 	b.buf = make([dynamic]byte, len, cap, allocator)
@@ -158,10 +165,11 @@ _builder_stream_vtable := &_builder_stream_vtable_obj
 /*
 Returns an io.Stream from a Builder
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 
-**Returns**  An io.Stream
+Returns:
+An io.Stream
 */
 to_stream :: proc(b: ^Builder) -> io.Stream {
 	return io.Stream{stream_vtable=_builder_stream_vtable, stream_data=b}
@@ -169,10 +177,11 @@ to_stream :: proc(b: ^Builder) -> io.Stream {
 /*
 Returns an io.Writer from a Builder
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 
-**Returns**   An io.Writer
+Returns: 
+An io.Writer
 */
 to_writer :: proc(b: ^Builder) -> io.Writer {
 	return io.to_writer(to_stream(b))
@@ -180,7 +189,7 @@ to_writer :: proc(b: ^Builder) -> io.Writer {
 /*
 Deletes the Builder byte buffer content
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 */
 builder_destroy :: proc(b: ^Builder) {
@@ -190,7 +199,7 @@ builder_destroy :: proc(b: ^Builder) {
 /*
 Reserves the Builder byte buffer to a specific capacity, when it's higher than before
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - cap: The desired capacity for the Builder's buffer
 */
@@ -200,7 +209,7 @@ builder_grow :: proc(b: ^Builder, cap: int) {
 /*
 Clears the Builder byte buffer content (sets len to zero)
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 */
 builder_reset :: proc(b: ^Builder) {
@@ -211,18 +220,23 @@ Creates a Builder from a slice of bytes with the same slice length as its capaci
 
 *Uses Nil Allocator - Does NOT allocate*
 
-**Inputs**  
+Inputs:
 - backing: A slice of bytes to be used as the backing buffer
 
+Returns:
+A new Builder
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
-	strings_builder_from_bytes_example :: proc() {
+	builder_from_bytes_example :: proc() {
 		bytes: [8]byte // <-- gets filled
 		builder := strings.builder_from_bytes(bytes[:])
-		fmt.println(strings.write_byte(&builder, 'a')) // -> "a"
-		fmt.println(strings.write_byte(&builder, 'b')) // -> "ab"
+		strings.write_byte(&builder, 'a')
+		fmt.println(strings.to_string(builder)) // -> "a"
+		strings.write_byte(&builder, 'b')
+		fmt.println(strings.to_string(builder)) // -> "ab"
 	}
 
 Output:
@@ -230,7 +244,6 @@ Output:
 	a
 	ab
 
-**Returns**  A new Builder
 */
 builder_from_bytes :: proc(backing: []byte) -> Builder {
 	s := transmute(runtime.Raw_Slice)backing
@@ -249,10 +262,11 @@ builder_from_slice :: builder_from_bytes
 /*
 Casts the Builder byte buffer to a string and returns it
 
-**Inputs**  
+Inputs:
 - b: A Builder
 
-**Returns**  The contents of the Builder's buffer, as a string
+Returns:
+The contents of the Builder's buffer, as a string
 */
 to_string :: proc(b: Builder) -> string {
 	return string(b.buf[:])
@@ -260,10 +274,11 @@ to_string :: proc(b: Builder) -> string {
 /*
 Returns the length of the Builder's buffer, in bytes
 
-**Inputs**  
+Inputs:
 - b: A Builder
 
-**Returns**  The length of the Builder's buffer
+Returns:
+The length of the Builder's buffer
 */
 builder_len :: proc(b: Builder) -> int {
 	return len(b.buf)
@@ -271,10 +286,11 @@ builder_len :: proc(b: Builder) -> int {
 /*
 Returns the capacity of the Builder's buffer, in bytes
 
-**Inputs**  
+Inputs:
 - b: A Builder
 
-**Returns**  The capacity of the Builder's buffer
+Returns:
+The capacity of the Builder's buffer
 */
 builder_cap :: proc(b: Builder) -> int {
 	return cap(b.buf)
@@ -282,10 +298,11 @@ builder_cap :: proc(b: Builder) -> int {
 /*
 The free space left in the Builder's buffer, in bytes
 
-**Inputs**  
+Inputs:
 - b: A Builder
 
-**Returns**  The available space left in the Builder's buffer
+Returns:
+The available space left in the Builder's buffer
 */
 builder_space :: proc(b: Builder) -> int {
 	return cap(b.buf) - len(b.buf)
@@ -293,16 +310,21 @@ builder_space :: proc(b: Builder) -> int {
 /*
 Appends a byte to the Builder and returns the number of bytes appended
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - x: The byte to be appended
 
+Returns:
+The number of bytes appended
+
+NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_write_byte_example :: proc() {
+	write_byte_example :: proc() {
 		builder := strings.builder_make()
 		strings.write_byte(&builder, 'a')        // 1
 		strings.write_byte(&builder, 'b')        // 1
@@ -313,9 +335,6 @@ Output:
 
 	ab
 
-NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
-
-**Returns**  The number of bytes appended
 */
 write_byte :: proc(b: ^Builder, x: byte) -> (n: int) {
 	n0 := len(b.buf)
@@ -326,7 +345,7 @@ write_byte :: proc(b: ^Builder, x: byte) -> (n: int) {
 /*
 Appends a slice of bytes to the Builder and returns the number of bytes appended
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - x: The slice of bytes to be appended
 
@@ -335,7 +354,7 @@ Example:
 	import "core:fmt"
 	import "core:strings"
 
-	strings_write_bytes_example :: proc() {
+	write_bytes_example :: proc() {
 		builder := strings.builder_make()
 		bytes := [?]byte { 'a', 'b', 'c' }
 		strings.write_bytes(&builder, bytes[:]) // 3
@@ -344,7 +363,8 @@ Example:
 
 NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
 
-**Returns**  The number of bytes appended
+Returns:
+The number of bytes appended
 */
 write_bytes :: proc(b: ^Builder, x: []byte) -> (n: int) {
 	n0 := len(b.buf)
@@ -355,16 +375,21 @@ write_bytes :: proc(b: ^Builder, x: []byte) -> (n: int) {
 /*
 Appends a single rune to the Builder and returns the number of bytes written and an `io.Error`
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - r: The rune to be appended
 
+Returns:
+The number of bytes written and an io.Error (if any)
+
+NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_write_rune_example :: proc() {
+	write_rune_example :: proc() {
 		builder := strings.builder_make()
 		strings.write_rune(&builder, 'ä')     // 2 None
 		strings.write_rune(&builder, 'b')       // 1 None
@@ -375,9 +400,6 @@ Output:
 
 	äb
 
-NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
-
-**Returns**  The number of bytes written and an io.Error (if any)
 */
 write_rune :: proc(b: ^Builder, r: rune) -> (int, io.Error) {
 	return io.write_rune(to_writer(b), r)
@@ -385,16 +407,21 @@ write_rune :: proc(b: ^Builder, r: rune) -> (int, io.Error) {
 /*
 Appends a quoted rune to the Builder and returns the number of bytes written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - r: The rune to be appended
 
+Returns:
+The number of bytes written
+
+NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_write_quoted_rune_example :: proc() {
+	write_quoted_rune_example :: proc() {
 		builder := strings.builder_make()
 		strings.write_string(&builder, "abc")      // 3
 		strings.write_quoted_rune(&builder, 'ä') // 4
@@ -406,9 +433,6 @@ Output:
 
 	abc'ä'abc
 
-NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
-
-**Returns**  The number of bytes written
 */
 write_quoted_rune :: proc(b: ^Builder, r: rune) -> (n: int) {
 	return io.write_quoted_rune(to_writer(b), r)
@@ -416,16 +440,21 @@ write_quoted_rune :: proc(b: ^Builder, r: rune) -> (n: int) {
 /*
 Appends a string to the Builder and returns the number of bytes written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - s: The string to be appended
 
+Returns:
+The number of bytes written
+
+NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_write_string_example :: proc() {
+	write_string_example :: proc() {
 		builder := strings.builder_make()
 		strings.write_string(&builder, "a")     // 1
 		strings.write_string(&builder, "bc")    // 2
@@ -436,9 +465,6 @@ Output:
 
 	abc
 
-NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
-
-**Returns**  The number of bytes written
 */
 write_string :: proc(b: ^Builder, s: string) -> (n: int) {
 	n0 := len(b.buf)
@@ -449,10 +475,11 @@ write_string :: proc(b: ^Builder, s: string) -> (n: int) {
 /*
 Pops and returns the last byte in the Builder or 0 when the Builder is empty
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 
-**Returns**  The last byte in the Builder or 0 if empty
+Returns:
+The last byte in the Builder or 0 if empty
 */
 pop_byte :: proc(b: ^Builder) -> (r: byte) {
 	if len(b.buf) == 0 {
@@ -467,10 +494,11 @@ pop_byte :: proc(b: ^Builder) -> (r: byte) {
 /*
 Pops the last rune in the Builder and returns the popped rune and its rune width or (0, 0) if empty
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 
-**Returns**  The popped rune and its rune width or (0, 0) if empty
+Returns:
+The popped rune and its rune width or (0, 0) if empty
 */
 pop_rune :: proc(b: ^Builder) -> (r: rune, width: int) {
 	if len(b.buf) == 0 {
@@ -485,17 +513,22 @@ pop_rune :: proc(b: ^Builder) -> (r: rune, width: int) {
 @(private)
 DIGITS_LOWER := "0123456789abcdefx"
 /*
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - str: The string to be quoted and appended
 - quote: The optional quote character (default is double quotes)
 
+Returns:
+The number of bytes written
+
+NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_write_quoted_string_example :: proc() {
+	write_quoted_string_example :: proc() {
 		builder := strings.builder_make()
 		strings.write_quoted_string(&builder, "a")        // 3
 		strings.write_quoted_string(&builder, "bc", '\'') // 4
@@ -507,9 +540,6 @@ Output:
 
 	"a"'bc'"xyz"
 
-NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
-
-**Returns**  The number of bytes written
 */
 write_quoted_string :: proc(b: ^Builder, str: string, quote: byte = '"') -> (n: int) {
 	n, _ = io.write_quoted_string(to_writer(b), str, quote)
@@ -518,11 +548,16 @@ write_quoted_string :: proc(b: ^Builder, str: string, quote: byte = '"') -> (n:
 /*
 Appends a rune to the Builder and returns the number of bytes written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - r: The rune to be appended
 - write_quote: Optional boolean flag to wrap in single-quotes (') (default is true)
 
+Returns:
+The number of bytes written
+
+NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
+
 Example:
 
 	import "core:fmt"
@@ -540,9 +575,6 @@ Output:
 
 	a'"'x
 
-NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
-
-**Returns**  The number of bytes written
 */
 write_encoded_rune :: proc(b: ^Builder, r: rune, write_quote := true) -> (n: int) {
 	n, _ = io.write_encoded_rune(to_writer(b), r, write_quote)
@@ -552,7 +584,7 @@ write_encoded_rune :: proc(b: ^Builder, r: rune, write_quote := true) -> (n: int
 /*
 Appends an escaped rune to the Builder and returns the number of bytes written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - r: The rune to be appended
 - quote: The quote character
@@ -565,7 +597,8 @@ Appends an escaped rune to the Builder and returns the number of bytes written
 
 NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
 
-**Returns**  The number of bytes written
+Returns:
+The number of bytes written
 */
 write_escaped_rune :: proc(b: ^Builder, r: rune, quote: byte, html_safe := false) -> (n: int) {
 	n, _ = io.write_escaped_rune(to_writer(b), r, quote, html_safe)
@@ -574,7 +607,7 @@ write_escaped_rune :: proc(b: ^Builder, r: rune, quote: byte, html_safe := false
 /*
 Writes a f64 value to the Builder and returns the number of characters written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - f: The f64 value to be appended
 - fmt: The format byte
@@ -584,7 +617,8 @@ Writes a f64 value to the Builder and returns the number of characters written
 
 NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
 
-**Returns**  The number of characters written
+Returns:
+The number of characters written
 */
 write_float :: proc(b: ^Builder, f: f64, fmt: byte, prec, bit_size: int, always_signed := false) -> (n: int) {
 	buf: [384]byte
@@ -599,7 +633,7 @@ write_float :: proc(b: ^Builder, f: f64, fmt: byte, prec, bit_size: int, always_
 /*
 Writes a f16 value to the Builder and returns the number of characters written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - f: The f16 value to be appended
 - fmt: The format byte
@@ -607,7 +641,8 @@ Writes a f16 value to the Builder and returns the number of characters written
 
 NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
 
-**Returns**  The number of characters written
+Returns:
+The number of characters written
 */
 write_f16 :: proc(b: ^Builder, f: f16, fmt: byte, always_signed := false) -> (n: int) {
 	buf: [384]byte
@@ -620,18 +655,23 @@ write_f16 :: proc(b: ^Builder, f: f16, fmt: byte, always_signed := false) -> (n:
 /*
 Writes a f32 value to the Builder and returns the number of characters written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - f: The f32 value to be appended
 - fmt: The format byte
 - always_signed: Optional boolean flag to always include the sign
 
+Returns:
+The number of characters written
+
+NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_write_f32_example :: proc() {
+	write_f32_example :: proc() {
 		builder := strings.builder_make()
 		strings.write_f32(&builder, 3.14159, 'f') // 6
 		strings.write_string(&builder, " - ")     // 3
@@ -643,9 +683,6 @@ Output:
 
 	3.14159012 - -1.23000003e-01
 
-NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
-
-**Returns**  The number of characters written
 */
 write_f32 :: proc(b: ^Builder, f: f32, fmt: byte, always_signed := false) -> (n: int) {
 	buf: [384]byte
@@ -658,7 +695,7 @@ write_f32 :: proc(b: ^Builder, f: f32, fmt: byte, always_signed := false) -> (n:
 /*
 Writes a f32 value to the Builder and returns the number of characters written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - f: The f32 value to be appended
 - fmt: The format byte
@@ -666,7 +703,8 @@ Writes a f32 value to the Builder and returns the number of characters written
 
 NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
 
-**Returns**  The number of characters written
+Returns:
+The number of characters written
 */
 write_f64 :: proc(b: ^Builder, f: f64, fmt: byte, always_signed := false) -> (n: int) {
 	buf: [384]byte
@@ -679,14 +717,15 @@ write_f64 :: proc(b: ^Builder, f: f64, fmt: byte, always_signed := false) -> (n:
 /*
 Writes a u64 value to the Builder and returns the number of characters written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - i: The u64 value to be appended
 - base: The optional base for the numeric representation
 
 NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
 
-**Returns**  The number of characters written
+Returns:
+The number of characters written
 */
 write_u64 :: proc(b: ^Builder, i: u64, base: int = 10) -> (n: int) {
 	buf: [32]byte
@@ -696,14 +735,15 @@ write_u64 :: proc(b: ^Builder, i: u64, base: int = 10) -> (n: int) {
 /*
 Writes a i64 value to the Builder and returns the number of characters written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - i: The i64 value to be appended
 - base: The optional base for the numeric representation
 
 NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
 
-**Returns**  The number of characters written
+Returns:
+The number of characters written
 */
 write_i64 :: proc(b: ^Builder, i: i64, base: int = 10) -> (n: int) {
 	buf: [32]byte
@@ -713,14 +753,15 @@ write_i64 :: proc(b: ^Builder, i: i64, base: int = 10) -> (n: int) {
 /*
 Writes a uint value to the Builder and returns the number of characters written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - i: The uint value to be appended
 - base: The optional base for the numeric representation
 
 NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
 
-**Returns**  The number of characters written
+Returns:
+The number of characters written
 */
 write_uint :: proc(b: ^Builder, i: uint, base: int = 10) -> (n: int) {
 	return write_u64(b, u64(i), base)
@@ -728,14 +769,15 @@ write_uint :: proc(b: ^Builder, i: uint, base: int = 10) -> (n: int) {
 /*
 Writes a int value to the Builder and returns the number of characters written
 
-**Inputs**  
+Inputs:
 - b: A pointer to the Builder
 - i: The int value to be appended
 - base: The optional base for the numeric representation
 
 NOTE: The backing dynamic array may be fixed in capacity or fail to resize, `n` states the number actually written.
 
-**Returns**  The number of characters written
+Returns:
+The number of characters written
 */
 write_int :: proc(b: ^Builder, i: int, base: int = 10) -> (n: int) {
 	return write_i64(b, i64(i), base)

+ 58 - 38
core/strings/conversion.odin

@@ -9,14 +9,15 @@ Converts invalid UTF-8 sequences in the input string `s` to the `replacement` st
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: Input string that may contain invalid UTF-8 sequences.
 - replacement: String to replace invalid UTF-8 sequences with.
 - allocator: (default: context.allocator).
 
 WARNING: Allocation does not occur when len(s) == 0
 
-**Returns**  A valid UTF-8 string with invalid sequences replaced by `replacement`.
+Returns:
+A valid UTF-8 string with invalid sequences replaced by `replacement`.
 */
 to_valid_utf8 :: proc(s, replacement: string, allocator := context.allocator) -> string {
 	if len(s) == 0 {
@@ -76,16 +77,19 @@ Converts the input string `s` to all lowercase characters.
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: Input string to be converted.
 - allocator: (default: context.allocator).
 
+Returns:
+A new string with all characters converted to lowercase.
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_to_lower_example :: proc() {
+	to_lower_example :: proc() {
 		fmt.println(strings.to_lower("TeST"))
 	}
 
@@ -93,7 +97,6 @@ Output:
 
 	test
 
-**Returns**  A new string with all characters converted to lowercase.
 */
 to_lower :: proc(s: string, allocator := context.allocator) -> string {
 	b: Builder
@@ -108,16 +111,19 @@ Converts the input string `s` to all uppercase characters.
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: Input string to be converted.
 - allocator: (default: context.allocator).
 
+Returns:
+A new string with all characters converted to uppercase.
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_to_upper_example :: proc() {
+	to_upper_example :: proc() {
 		fmt.println(strings.to_upper("Test"))
 	}
 
@@ -125,7 +131,6 @@ Output:
 
 	TEST
 
-**Returns**  A new string with all characters converted to uppercase.
 */
 to_upper :: proc(s: string, allocator := context.allocator) -> string {
 	b: Builder
@@ -138,10 +143,11 @@ to_upper :: proc(s: string, allocator := context.allocator) -> string {
 /*
 Checks if the rune `r` is a delimiter (' ', '-', or '_').
 
-**Inputs**  
+Inputs:
 - r: Rune to check for delimiter status.
 
-**Returns**  True if `r` is a delimiter, false otherwise.
+Returns:
+True if `r` is a delimiter, false otherwise.
 */
 is_delimiter :: proc(r: rune) -> bool {
 	return r == '-' || r == '_' || is_space(r)
@@ -149,10 +155,11 @@ is_delimiter :: proc(r: rune) -> bool {
 /*
 Checks if the rune `r` is a non-alphanumeric or space character.
 
-**Inputs**  
+Inputs:
 - r: Rune to check for separator status.
 
-**Returns**  True if `r` is a non-alpha or `unicode.is_space` rune.
+Returns:
+True if `r` is a non-alpha or `unicode.is_space` rune.
 */
 is_separator :: proc(r: rune) -> bool {
 	if r <= 0x7f {
@@ -179,7 +186,7 @@ is_separator :: proc(r: rune) -> bool {
 /*
 Iterates over a string, calling a callback for each rune with the previous, current, and next runes as arguments.
 
-**Inputs**  
+Inputs:
 - w: An io.Writer to be used by the callback for writing output.
 - s: The input string to be iterated over.
 - callback: A procedure to be called for each rune in the string, with arguments (w: io.Writer, prev, curr, next: rune).
@@ -191,7 +198,7 @@ Example:
 	import "core:strings"
 	import "core:io"
 
-	strings_string_case_iterator_example :: proc() {
+	string_case_iterator_example :: proc() {
 		my_callback :: proc(w: io.Writer, prev, curr, next: rune) {
 			fmt.println("my_callback", curr) // <-- Custom logic here
 		}
@@ -241,11 +248,12 @@ Converts the input string `s` to "lowerCamelCase".
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: Input string to be converted.
 - allocator: (default: context.allocator).
 
-**Returns**  A "lowerCamelCase" formatted string.
+Returns:
+A "lowerCamelCase" formatted string.
 */
 to_camel_case :: proc(s: string, allocator := context.allocator) -> string {
 	s := s
@@ -275,11 +283,12 @@ Converts the input string `s` to "UpperCamelCase" (PascalCase).
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: Input string to be converted.
 - allocator: (default: context.allocator).
 
-**Returns**  A "PascalCase" formatted string.
+Returns:
+A "PascalCase" formatted string.
 */
 to_pascal_case :: proc(s: string, allocator := context.allocator) -> string {
 	s := s
@@ -307,18 +316,21 @@ Returns a string converted to a delimiter-separated case with configurable casin
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: The input string to be converted
 - delimiter: The rune to be used as the delimiter between words
 - all_upper_case: A boolean indicating if the output should be all uppercased (true) or lowercased (false)
 - allocator: (default: context.allocator).
 
+Returns:
+The converted string
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_to_delimiter_case_example :: proc() {
+	to_delimiter_case_example :: proc() {
 		fmt.println(strings.to_delimiter_case("Hello World", '_', false))
 		fmt.println(strings.to_delimiter_case("Hello World", ' ', true))
 		fmt.println(strings.to_delimiter_case("aBC", '_', false))
@@ -328,9 +340,8 @@ Output:
 
 	hello_world
 	HELLO WORLD
-	a_b_c
+	a_bc
 
-**Returns**  The converted string
 */
 to_delimiter_case :: proc(
 	s: string,
@@ -380,16 +391,19 @@ Converts a string to "snake_case" with all runes lowercased
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: The input string to be converted
 - allocator: (default: context.allocator).
 
+Returns:
+The converted string
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_to_snake_case_example :: proc() {
+	to_snake_case_example :: proc() {
 		fmt.println(strings.to_snake_case("HelloWorld"))
 		fmt.println(strings.to_snake_case("Hello World"))
 	}
@@ -399,8 +413,6 @@ Output:
 	hello_world
 	hello_world
 
-```
-**Returns**  The converted string
 */
 to_snake_case :: proc(s: string, allocator := context.allocator) -> string {
 	return to_delimiter_case(s, '_', false, allocator)
@@ -412,16 +424,19 @@ Converts a string to "SNAKE_CASE" with all runes uppercased
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: The input string to be converted
 - allocator: (default: context.allocator).
 
+Returns:
+The converted string
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_to_upper_snake_case_example :: proc() {
+	to_upper_snake_case_example :: proc() {
 		fmt.println(strings.to_upper_snake_case("HelloWorld"))
 	}
 
@@ -429,7 +444,6 @@ Output:
 
 	HELLO_WORLD
 
-**Returns**  The converted string
 */
 to_upper_snake_case :: proc(s: string, allocator := context.allocator) -> string {
 	return to_delimiter_case(s, '_', true, allocator)
@@ -439,16 +453,19 @@ Converts a string to "kebab-case" with all runes lowercased
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: The input string to be converted
 - allocator: (default: context.allocator).
 
+Returns:
+The converted string
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_to_kebab_case_example :: proc() {
+	to_kebab_case_example :: proc() {
 		fmt.println(strings.to_kebab_case("HelloWorld"))
 	}
 
@@ -456,7 +473,6 @@ Output:
 
 	hello-world
 
-**Returns**  The converted string
 */
 to_kebab_case :: proc(s: string, allocator := context.allocator) -> string {
 	return to_delimiter_case(s, '-', false, allocator)
@@ -466,16 +482,19 @@ Converts a string to "KEBAB-CASE" with all runes uppercased
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: The input string to be converted
 - allocator: (default: context.allocator).
 
+Returns:
+The converted string
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_to_upper_kebab_case_example :: proc() {
+	to_upper_kebab_case_example :: proc() {
 		fmt.println(strings.to_upper_kebab_case("HelloWorld"))
 	}
 
@@ -483,7 +502,6 @@ Output:
 
 	HELLO-WORLD
 
-**Returns**  The converted string
 */
 to_upper_kebab_case :: proc(s: string, allocator := context.allocator) -> string {
 	return to_delimiter_case(s, '-', true, allocator)
@@ -493,16 +511,19 @@ Converts a string to "Ada_Case"
 
 *Allocates Using Provided Allocator*
 
-**Inputs**  
+Inputs:
 - s: The input string to be converted
 - allocator: (default: context.allocator).
 
+Returns:
+The converted string
+
 Example:
 
 	import "core:fmt"
 	import "core:strings"
 
-	strings_to_upper_kebab_case_example :: proc() {
+	to_ada_case_example :: proc() {
 		fmt.println(strings.to_ada_case("HelloWorld"))
 	}
 
@@ -510,7 +531,6 @@ Output:
 
 	Hello_World
 
-**Returns**  The converted string
 */
 to_ada_case :: proc(s: string, allocator := context.allocator) -> string {
 	s := s

+ 11 - 8
core/strings/intern.odin

@@ -25,7 +25,7 @@ Initializes the entries map and sets the allocator for the string entries
 
 *Allocates Using Provided Allocators*
 
-**Inputs**  
+Inputs:
 - m: A pointer to the Intern struct to be initialized
 - allocator: The allocator for the Intern_Entry strings (Default: context.allocator)
 - map_allocator: The allocator for the map of entries (Default: context.allocator)
@@ -37,7 +37,7 @@ intern_init :: proc(m: ^Intern, allocator := context.allocator, map_allocator :=
 /*
 Frees the map and all its content allocated using the `.allocator`.
 
-**Inputs**  
+Inputs:
 - m: A pointer to the Intern struct to be destroyed
 */
 intern_destroy :: proc(m: ^Intern) {
@@ -51,13 +51,14 @@ Returns an interned copy of the given text, adding it to the map if not already
 
 *Allocate using the Intern's Allocator (First time string is seen only)*
 
-**Inputs**  
+Inputs:
 - m: A pointer to the Intern struct
 - text: The string to be interned
 
 NOTE: The returned string lives as long as the map entry lives.
 
-**Returns**  The interned string and an allocator error if any
+Returns:
+The interned string and an allocator error if any
 */
 intern_get :: proc(m: ^Intern, text: string) -> (str: string, err: runtime.Allocator_Error) {
 	entry := _intern_get_entry(m, text) or_return
@@ -68,13 +69,14 @@ Returns an interned copy of the given text as a cstring, adding it to the map if
 
 *Allocate using the Intern's Allocator  (First time string is seen only)*
 
-**Inputs**  
+Inputs:
 - m: A pointer to the Intern struct
 - text: The string to be interned
 
 NOTE: The returned cstring lives as long as the map entry lives
 
-**Returns**  The interned cstring and an allocator error if any
+Returns:
+The interned cstring and an allocator error if any
 */
 intern_get_cstring :: proc(m: ^Intern, text: string) -> (str: cstring, err: runtime.Allocator_Error) {
 	entry := _intern_get_entry(m, text) or_return
@@ -86,11 +88,12 @@ Sets and allocates the entry if it wasn't set yet
 
 *Allocate using the Intern's Allocator  (First time string is seen only)*
 
-**Inputs**  
+Inputs:
 - m: A pointer to the Intern struct
 - text: The string to be looked up or interned
 
-**Returns**  The new or existing interned entry and an allocator error if any
+Returns:
+The new or existing interned entry and an allocator error if any
 */
 _intern_get_entry :: proc(m: ^Intern, text: string) -> (new_entry: ^Intern_Entry, err: runtime.Allocator_Error) #no_bounds_check {
 	if prev, ok := m.entries[text]; ok {

+ 34 - 27
core/strings/reader.odin

@@ -16,7 +16,7 @@ Reader :: struct {
 /*
 Initializes a string Reader with the provided string
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 - s: The input string to be read
 */
@@ -28,10 +28,11 @@ reader_init :: proc(r: ^Reader, s: string) {
 /*
 Converts a Reader into an `io.Stream`
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 
-**Returns**  An io.Stream for the given Reader
+Returns:
+An io.Stream for the given Reader
 */
 reader_to_stream :: proc(r: ^Reader) -> (s: io.Stream) {
 	s.stream_data = r
@@ -41,11 +42,12 @@ reader_to_stream :: proc(r: ^Reader) -> (s: io.Stream) {
 /*
 Initializes a string Reader and returns an `io.Reader` for the given string
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 - s: The input string to be read
 
-**Returns**  An io.Reader for the given string
+Returns:
+An io.Reader for the given string
 */
 to_reader :: proc(r: ^Reader, s: string) -> io.Reader {
 	reader_init(r, s)
@@ -55,11 +57,12 @@ to_reader :: proc(r: ^Reader, s: string) -> io.Reader {
 /*
 Initializes a string Reader and returns an `io.Reader_At` for the given string
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 - s: The input string to be read
 
-**Returns**  An `io.Reader_At` for the given string
+Returns:
+An `io.Reader_At` for the given string
 */
 to_reader_at :: proc(r: ^Reader, s: string) -> io.Reader_At {
 	reader_init(r, s)
@@ -69,10 +72,11 @@ to_reader_at :: proc(r: ^Reader, s: string) -> io.Reader_At {
 /*
 Returns the remaining length of the Reader
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 
-**Returns**  The remaining length of the Reader
+Returns:
+The remaining length of the Reader
 */
 reader_length :: proc(r: ^Reader) -> int {
 	if r.i >= i64(len(r.s)) {
@@ -83,10 +87,11 @@ reader_length :: proc(r: ^Reader) -> int {
 /*
 Returns the length of the string stored in the Reader
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 
-**Returns**  The length of the string stored in the Reader
+Returns:
+The length of the string stored in the Reader
 */
 reader_size :: proc(r: ^Reader) -> i64 {
 	return i64(len(r.s))
@@ -94,11 +99,11 @@ reader_size :: proc(r: ^Reader) -> i64 {
 /*
 Reads len(p) bytes from the Reader's string and copies into the provided slice.
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 - p: A byte slice to copy data into
 
-**Returns**  
+Returns:
 - n: The number of bytes read
 - err: An `io.Error` if an error occurs while reading, including `.EOF`, otherwise `nil` denotes success.
 */
@@ -114,12 +119,12 @@ reader_read :: proc(r: ^Reader, p: []byte) -> (n: int, err: io.Error) {
 /*
 Reads len(p) bytes from the Reader's string and copies into the provided slice, at the specified offset from the current index.
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 - p: A byte slice to copy data into
 - off: The offset from which to read
 
-**Returns**  
+Returns:
 - n: The number of bytes read
 - err: An `io.Error` if an error occurs while reading, including `.EOF`, otherwise `nil` denotes success.
 */
@@ -139,10 +144,10 @@ reader_read_at :: proc(r: ^Reader, p: []byte, off: i64) -> (n: int, err: io.Erro
 /*
 Reads and returns a single byte from the Reader's string
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 
-**Returns**  
+Returns:
 - The byte read from the Reader
 - err: An `io.Error` if an error occurs while reading, including `.EOF`, otherwise `nil` denotes success.
 */
@@ -158,10 +163,11 @@ reader_read_byte :: proc(r: ^Reader) -> (byte, io.Error) {
 /*
 Decrements the Reader's index (i) by 1
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 
-**Returns**  An `io.Error` if `r.i <= 0` (`.Invalid_Unread`), otherwise `nil` denotes success.
+Returns:
+An `io.Error` if `r.i <= 0` (`.Invalid_Unread`), otherwise `nil` denotes success.
 */
 reader_unread_byte :: proc(r: ^Reader) -> io.Error {
 	if r.i <= 0 {
@@ -174,10 +180,10 @@ reader_unread_byte :: proc(r: ^Reader) -> io.Error {
 /*
 Reads and returns a single rune and its `size` from the Reader's string
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 
-**Returns**  
+Returns:
 - rr: The rune read from the Reader
 - size: The size of the rune in bytes
 - err: An `io.Error` if an error occurs while reading
@@ -199,12 +205,13 @@ reader_read_rune :: proc(r: ^Reader) -> (rr: rune, size: int, err: io.Error) {
 /*
 Decrements the Reader's index (i) by the size of the last read rune
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 
 WARNING: May only be used once and after a valid `read_rune` call
 
-**Returns**  An `io.Error` if an error occurs while unreading (`.Invalid_Unread`), else `nil` denotes success.
+Returns:
+An `io.Error` if an error occurs while unreading (`.Invalid_Unread`), else `nil` denotes success.
 */
 reader_unread_rune :: proc(r: ^Reader) -> io.Error {
 	if r.i <= 0 {
@@ -220,12 +227,12 @@ reader_unread_rune :: proc(r: ^Reader) -> io.Error {
 /*
 Seeks the Reader's index to a new position
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 - offset: The new offset position
 - whence: The reference point for the new position (`.Start`, `.Current`, or `.End`)
 
-**Returns**  
+Returns:
 - The absolute offset after seeking
 - err: An `io.Error` if an error occurs while seeking (`.Invalid_Whence`, `.Invalid_Offset`)
 */
@@ -252,13 +259,13 @@ reader_seek :: proc(r: ^Reader, offset: i64, whence: io.Seek_From) -> (i64, io.E
 /*
 Writes the remaining content of the Reader's string into the provided `io.Writer`
 
-**Inputs**  
+Inputs:
 - r: A pointer to a Reader struct
 - w: The io.Writer to write the remaining content into
 
 WARNING: Panics if writer writes more bytes than remainig length of string.
 
-**Returns**  
+Returns:
 - n: The number of bytes written
 - err: An io.Error if an error occurs while writing (`.Short_Write`)
 */

File diff suppressed because it is too large
+ 187 - 129
core/strings/strings.odin


+ 0 - 22
core/sys/windows/types.odin

@@ -145,8 +145,6 @@ PCONDITION_VARIABLE :: ^CONDITION_VARIABLE
 PLARGE_INTEGER :: ^LARGE_INTEGER
 PSRWLOCK :: ^SRWLOCK
 
-MMRESULT :: UINT
-
 CREATE_WAITABLE_TIMER_MANUAL_RESET    :: 0x00000001
 CREATE_WAITABLE_TIMER_HIGH_RESOLUTION :: 0x00000002
 
@@ -261,26 +259,6 @@ GET_FILEEX_INFO_LEVELS :: distinct i32
 GetFileExInfoStandard: GET_FILEEX_INFO_LEVELS : 0
 GetFileExMaxInfoLevel: GET_FILEEX_INFO_LEVELS : 1
 
-// String resource number bases (internal use)
-
-MMSYSERR_BASE :: 0
-WAVERR_BASE   :: 32
-MIDIERR_BASE  :: 64
-TIMERR_BASE   :: 96
-JOYERR_BASE   :: 160
-MCIERR_BASE   :: 256
-MIXERR_BASE   :: 1024
-
-MCI_STRING_OFFSET :: 512
-MCI_VD_OFFSET     :: 1024
-MCI_CD_OFFSET     :: 1088
-MCI_WAVE_OFFSET   :: 1152
-MCI_SEQ_OFFSET    :: 1216
-
-// timer error return values
-TIMERR_NOERROR :: 0                // no error
-TIMERR_NOCANDO :: TIMERR_BASE + 1  // request not completed
-TIMERR_STRUCT  :: TIMERR_BASE + 33 // time struct size
 
 DIAGNOSTIC_REASON_VERSION :: 0
 

+ 161 - 0
core/sys/windows/winmm.odin

@@ -3,9 +3,170 @@ package sys_windows
 
 foreign import winmm "system:Winmm.lib"
 
+MMRESULT :: UINT
+
 @(default_calling_convention="stdcall")
 foreign winmm {
+	timeGetDevCaps  :: proc(ptc: LPTIMECAPS, cbtc: UINT) -> MMRESULT ---
 	timeBeginPeriod :: proc(uPeriod: UINT) -> MMRESULT ---
 	timeEndPeriod   :: proc(uPeriod: UINT) -> MMRESULT ---
 	timeGetTime     :: proc() -> DWORD ---
 }
+
+LPTIMECAPS :: ^TIMECAPS
+TIMECAPS :: struct {
+	wPeriodMin: UINT,
+	wPeriodMax: UINT,
+}
+
+// String resource number bases (internal use)
+MMSYSERR_BASE :: 0
+WAVERR_BASE   :: 32
+MIDIERR_BASE  :: 64
+TIMERR_BASE   :: 96
+JOYERR_BASE   :: 160
+MCIERR_BASE   :: 256
+MIXERR_BASE   :: 1024
+
+MCI_STRING_OFFSET :: 512
+MCI_VD_OFFSET     :: 1024
+MCI_CD_OFFSET     :: 1088
+MCI_WAVE_OFFSET   :: 1152
+MCI_SEQ_OFFSET    :: 1216
+
+/* general error return values */
+MMSYSERR_NOERROR      :: 0                  /* no error */
+MMSYSERR_ERROR        :: MMSYSERR_BASE + 1  /* unspecified error */
+MMSYSERR_BADDEVICEID  :: MMSYSERR_BASE + 2  /* device ID out of range */
+MMSYSERR_NOTENABLED   :: MMSYSERR_BASE + 3  /* driver failed enable */
+MMSYSERR_ALLOCATED    :: MMSYSERR_BASE + 4  /* device already allocated */
+MMSYSERR_INVALHANDLE  :: MMSYSERR_BASE + 5  /* device handle is invalid */
+MMSYSERR_NODRIVER     :: MMSYSERR_BASE + 6  /* no device driver present */
+MMSYSERR_NOMEM        :: MMSYSERR_BASE + 7  /* memory allocation error */
+MMSYSERR_NOTSUPPORTED :: MMSYSERR_BASE + 8  /* function isn't supported */
+MMSYSERR_BADERRNUM    :: MMSYSERR_BASE + 9  /* error value out of range */
+MMSYSERR_INVALFLAG    :: MMSYSERR_BASE + 10 /* invalid flag passed */
+MMSYSERR_INVALPARAM   :: MMSYSERR_BASE + 11 /* invalid parameter passed */
+MMSYSERR_HANDLEBUSY   :: MMSYSERR_BASE + 12 /* handle being used simultaneously on another thread (eg callback) */
+MMSYSERR_INVALIDALIAS :: MMSYSERR_BASE + 13 /* specified alias not found */
+MMSYSERR_BADDB        :: MMSYSERR_BASE + 14 /* bad registry database */
+MMSYSERR_KEYNOTFOUND  :: MMSYSERR_BASE + 15 /* registry key not found */
+MMSYSERR_READERROR    :: MMSYSERR_BASE + 16 /* registry read error */
+MMSYSERR_WRITEERROR   :: MMSYSERR_BASE + 17 /* registry write error */
+MMSYSERR_DELETEERROR  :: MMSYSERR_BASE + 18 /* registry delete error */
+MMSYSERR_VALNOTFOUND  :: MMSYSERR_BASE + 19 /* registry value not found */
+MMSYSERR_NODRIVERCB   :: MMSYSERR_BASE + 20 /* driver does not call DriverCallback */
+MMSYSERR_MOREDATA     :: MMSYSERR_BASE + 21 /* more data to be returned */
+MMSYSERR_LASTERROR    :: MMSYSERR_BASE + 21 /* last error in range */
+
+/* waveform audio error return values */
+WAVERR_BADFORMAT    :: WAVERR_BASE + 0 /* unsupported wave format */
+WAVERR_STILLPLAYING :: WAVERR_BASE + 1 /* still something playing */
+WAVERR_UNPREPARED   :: WAVERR_BASE + 2 /* header not prepared */
+WAVERR_SYNC         :: WAVERR_BASE + 3 /* device is synchronous */
+WAVERR_LASTERROR    :: WAVERR_BASE + 3 /* last error in range */
+
+/* MIDI error return values */
+MIDIERR_UNPREPARED    :: MIDIERR_BASE + 0 /* header not prepared */
+MIDIERR_STILLPLAYING  :: MIDIERR_BASE + 1 /* still something playing */
+MIDIERR_NOMAP         :: MIDIERR_BASE + 2 /* no configured instruments */
+MIDIERR_NOTREADY      :: MIDIERR_BASE + 3 /* hardware is still busy */
+MIDIERR_NODEVICE      :: MIDIERR_BASE + 4 /* port no longer connected */
+MIDIERR_INVALIDSETUP  :: MIDIERR_BASE + 5 /* invalid MIF */
+MIDIERR_BADOPENMODE   :: MIDIERR_BASE + 6 /* operation unsupported w/ open mode */
+MIDIERR_DONT_CONTINUE :: MIDIERR_BASE + 7 /* thru device 'eating' a message */
+MIDIERR_LASTERROR     :: MIDIERR_BASE + 7 /* last error in range */
+
+/* timer error return values */
+TIMERR_NOERROR :: 0                /* no error */
+TIMERR_NOCANDO :: TIMERR_BASE + 1  /* request not completed */
+TIMERR_STRUCT  :: TIMERR_BASE + 33 /* time struct size */
+
+/* joystick error return values */
+JOYERR_NOERROR   :: 0               /* no error */
+JOYERR_PARMS     :: JOYERR_BASE + 5 /* bad parameters */
+JOYERR_NOCANDO   :: JOYERR_BASE + 6 /* request not completed */
+JOYERR_UNPLUGGED :: JOYERR_BASE + 7 /* joystick is unplugged */
+
+/* MCI error return values */
+MCIERR_INVALID_DEVICE_ID        :: MCIERR_BASE + 1
+MCIERR_UNRECOGNIZED_KEYWORD     :: MCIERR_BASE + 3
+MCIERR_UNRECOGNIZED_COMMAND     :: MCIERR_BASE + 5
+MCIERR_HARDWARE                 :: MCIERR_BASE + 6
+MCIERR_INVALID_DEVICE_NAME      :: MCIERR_BASE + 7
+MCIERR_OUT_OF_MEMORY            :: MCIERR_BASE + 8
+MCIERR_DEVICE_OPEN              :: MCIERR_BASE + 9
+MCIERR_CANNOT_LOAD_DRIVER       :: MCIERR_BASE + 10
+MCIERR_MISSING_COMMAND_STRING   :: MCIERR_BASE + 11
+MCIERR_PARAM_OVERFLOW           :: MCIERR_BASE + 12
+MCIERR_MISSING_STRING_ARGUMENT  :: MCIERR_BASE + 13
+MCIERR_BAD_INTEGER              :: MCIERR_BASE + 14
+MCIERR_PARSER_INTERNAL          :: MCIERR_BASE + 15
+MCIERR_DRIVER_INTERNAL          :: MCIERR_BASE + 16
+MCIERR_MISSING_PARAMETER        :: MCIERR_BASE + 17
+MCIERR_UNSUPPORTED_FUNCTION     :: MCIERR_BASE + 18
+MCIERR_FILE_NOT_FOUND           :: MCIERR_BASE + 19
+MCIERR_DEVICE_NOT_READY         :: MCIERR_BASE + 20
+MCIERR_INTERNAL                 :: MCIERR_BASE + 21
+MCIERR_DRIVER                   :: MCIERR_BASE + 22
+MCIERR_CANNOT_USE_ALL           :: MCIERR_BASE + 23
+MCIERR_MULTIPLE                 :: MCIERR_BASE + 24
+MCIERR_EXTENSION_NOT_FOUND      :: MCIERR_BASE + 25
+MCIERR_OUTOFRANGE               :: MCIERR_BASE + 26
+MCIERR_FLAGS_NOT_COMPATIBLE     :: MCIERR_BASE + 28
+MCIERR_FILE_NOT_SAVED           :: MCIERR_BASE + 30
+MCIERR_DEVICE_TYPE_REQUIRED     :: MCIERR_BASE + 31
+MCIERR_DEVICE_LOCKED            :: MCIERR_BASE + 32
+MCIERR_DUPLICATE_ALIAS          :: MCIERR_BASE + 33
+MCIERR_BAD_CONSTANT             :: MCIERR_BASE + 34
+MCIERR_MUST_USE_SHAREABLE       :: MCIERR_BASE + 35
+MCIERR_MISSING_DEVICE_NAME      :: MCIERR_BASE + 36
+MCIERR_BAD_TIME_FORMAT          :: MCIERR_BASE + 37
+MCIERR_NO_CLOSING_QUOTE         :: MCIERR_BASE + 38
+MCIERR_DUPLICATE_FLAGS          :: MCIERR_BASE + 39
+MCIERR_INVALID_FILE             :: MCIERR_BASE + 40
+MCIERR_NULL_PARAMETER_BLOCK     :: MCIERR_BASE + 41
+MCIERR_UNNAMED_RESOURCE         :: MCIERR_BASE + 42
+MCIERR_NEW_REQUIRES_ALIAS       :: MCIERR_BASE + 43
+MCIERR_NOTIFY_ON_AUTO_OPEN      :: MCIERR_BASE + 44
+MCIERR_NO_ELEMENT_ALLOWED       :: MCIERR_BASE + 45
+MCIERR_NONAPPLICABLE_FUNCTION   :: MCIERR_BASE + 46
+MCIERR_ILLEGAL_FOR_AUTO_OPEN    :: MCIERR_BASE + 47
+MCIERR_FILENAME_REQUIRED        :: MCIERR_BASE + 48
+MCIERR_EXTRA_CHARACTERS         :: MCIERR_BASE + 49
+MCIERR_DEVICE_NOT_INSTALLED     :: MCIERR_BASE + 50
+MCIERR_GET_CD                   :: MCIERR_BASE + 51
+MCIERR_SET_CD                   :: MCIERR_BASE + 52
+MCIERR_SET_DRIVE                :: MCIERR_BASE + 53
+MCIERR_DEVICE_LENGTH            :: MCIERR_BASE + 54
+MCIERR_DEVICE_ORD_LENGTH        :: MCIERR_BASE + 55
+MCIERR_NO_INTEGER               :: MCIERR_BASE + 56
+MCIERR_WAVE_OUTPUTSINUSE        :: MCIERR_BASE + 64
+MCIERR_WAVE_SETOUTPUTINUSE      :: MCIERR_BASE + 65
+MCIERR_WAVE_INPUTSINUSE         :: MCIERR_BASE + 66
+MCIERR_WAVE_SETINPUTINUSE       :: MCIERR_BASE + 67
+MCIERR_WAVE_OUTPUTUNSPECIFIED   :: MCIERR_BASE + 68
+MCIERR_WAVE_INPUTUNSPECIFIED    :: MCIERR_BASE + 69
+MCIERR_WAVE_OUTPUTSUNSUITABLE   :: MCIERR_BASE + 70
+MCIERR_WAVE_SETOUTPUTUNSUITABLE :: MCIERR_BASE + 71
+MCIERR_WAVE_INPUTSUNSUITABLE    :: MCIERR_BASE + 72
+MCIERR_WAVE_SETINPUTUNSUITABLE  :: MCIERR_BASE + 73
+MCIERR_SEQ_DIV_INCOMPATIBLE     :: MCIERR_BASE + 80
+MCIERR_SEQ_PORT_INUSE           :: MCIERR_BASE + 81
+MCIERR_SEQ_PORT_NONEXISTENT     :: MCIERR_BASE + 82
+MCIERR_SEQ_PORT_MAPNODEVICE     :: MCIERR_BASE + 83
+MCIERR_SEQ_PORT_MISCERROR       :: MCIERR_BASE + 84
+MCIERR_SEQ_TIMER                :: MCIERR_BASE + 85
+MCIERR_SEQ_PORTUNSPECIFIED      :: MCIERR_BASE + 86
+MCIERR_SEQ_NOMIDIPRESENT        :: MCIERR_BASE + 87
+MCIERR_NO_WINDOW                :: MCIERR_BASE + 90
+MCIERR_CREATEWINDOW             :: MCIERR_BASE + 91
+MCIERR_FILE_READ                :: MCIERR_BASE + 92
+MCIERR_FILE_WRITE               :: MCIERR_BASE + 93
+MCIERR_NO_IDENTITY              :: MCIERR_BASE + 94
+
+/*  MMRESULT error return values specific to the mixer API */
+MIXERR_INVALLINE    :: (MIXERR_BASE + 0)
+MIXERR_INVALCONTROL :: (MIXERR_BASE + 1)
+MIXERR_INVALVALUE   :: (MIXERR_BASE + 2)
+MIXERR_LASTERROR    :: (MIXERR_BASE + 2)

+ 100 - 0
core/text/table/doc.odin

@@ -0,0 +1,100 @@
+/*
+	package table implements ascii/markdown/html/custom rendering of tables.
+
+	---
+
+	Custom rendering example:
+
+	```odin
+	tbl := init(&Table{})
+	padding(tbl, 0, 1)
+	row(tbl, "A_LONG_ENUM", "= 54,", "// A comment about A_LONG_ENUM")
+	row(tbl, "AN_EVEN_LONGER_ENUM", "= 1,", "// A comment about AN_EVEN_LONGER_ENUM")
+	build(tbl)
+	for row in 0..<tbl.nr_rows {
+		for col in 0..<tbl.nr_cols {
+			write_table_cell(stdio_writer(), tbl, row, col)
+		}
+		io.write_byte(stdio_writer(), '\n')
+	}
+	```
+
+	This outputs:
+	```
+	A_LONG_ENUM         = 54, // A comment about A_LONG_ENUM
+	AN_EVEN_LONGER_ENUM = 1,  // A comment about AN_EVEN_LONGER_ENUM
+	```
+
+	---
+
+	ASCII rendering example:
+
+	```odin
+	tbl := init(&Table{})
+	defer destroy(tbl)
+
+	caption(tbl, "This is a table caption and it is very long")
+
+	padding(tbl, 1, 1) // Left/right padding of cells
+
+	header(tbl, "AAAAAAAAA", "B")
+	header(tbl, "C") // Appends to previous header row. Same as if done header("AAAAAAAAA", "B", "C") from start.
+
+	// Create a row with two values. Since there are three columns the third
+	// value will become the empty string.
+	//
+	// NOTE: header() is not allowed anymore after this.
+	row(tbl, 123, "foo")
+
+	// Use `format()` if you need custom formatting. This will allocate into
+	// the arena specified at init.
+	row(tbl,
+	    format(tbl, "%09d", 5),
+	    format(tbl, "%.6f", 6.28318530717958647692528676655900576))
+
+	// A row with zero values is allowed as long as a previous row or header
+	// exist. The value and alignment of each cell can then be set
+	// individually.
+	row(tbl)
+		set_cell_value_and_alignment(tbl, last_row(tbl), 0, "a", .Center)
+		set_cell_value(tbl, last_row(tbl), 1, "bbb")
+		set_cell_value(tbl, last_row(tbl), 2, "c")
+
+	// Headers are regular cells, too. Use header_row() as row index to modify
+	// header cells.
+	set_cell_alignment(tbl, header_row(tbl), 1, .Center) // Sets alignment of 'B' column to Center.
+	set_cell_alignment(tbl, header_row(tbl), 2, .Right) // Sets alignment of 'C' column to Right.
+
+	build(tbl)
+
+	write_ascii_table(stdio_writer(), tbl)
+	write_markdown_table(stdio_writer(), tbl)
+	```
+
+	This outputs:
+	```
+	+-----------------------------------------------+
+	|  This is a table caption and it is very long  |
+	+------------------+-----------------+----------+
+	| AAAAAAAAA        |        B        |        C |
+	+------------------+-----------------+----------+
+	| 123              | foo             |          |
+	| 000000005        | 6.283185        |          |
+	|        a         | bbb             | c        |
+	+------------------+-----------------+----------+
+	```
+
+	and
+
+	```
+	|    AAAAAAAAA     |        B        |    C     |
+	|:-----------------|:---------------:|---------:|
+	| 123              | foo             |          |
+	| 000000005        | 6.283185        |          |
+	| a                | bbb             | c        |
+	```
+
+	respectively.
+*/
+
+package text_table

+ 384 - 0
core/text/table/table.odin

@@ -0,0 +1,384 @@
+/*
+	Copyright 2023 oskarnp <[email protected]>
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		oskarnp: Initial implementation.
+*/
+
+package text_table
+
+import "core:io"
+import "core:os"
+import "core:fmt"
+import "core:mem"
+import "core:mem/virtual"
+import "core:runtime"
+import "core:strings"
+
+Cell :: struct {
+	text: string,
+	alignment: Cell_Alignment,
+}
+
+Cell_Alignment :: enum {
+	Left,
+	Center,
+	Right,
+}
+
+Table :: struct {
+	lpad, rpad: int, // Cell padding (left/right)
+	cells: [dynamic]Cell,
+	caption: string,
+	nr_rows, nr_cols: int,
+	has_header_row: bool,
+	table_allocator: runtime.Allocator,  // Used for allocating cells/colw
+	format_allocator: runtime.Allocator, // Used for allocating Cell.text when applicable
+
+	dirty: bool, // True if build() needs to be called before rendering
+
+	// The following are computed on build()
+	colw: [dynamic]int, // Width of each column (including padding, excluding borders)
+	tblw: int,          // Width of entire table (including padding, excluding borders)
+}
+
+init :: proc{init_with_allocator, init_with_virtual_arena, init_with_mem_arena}
+
+init_with_allocator :: proc(tbl: ^Table, format_allocator := context.temp_allocator, table_allocator := context.allocator) -> ^Table {
+	tbl.table_allocator = table_allocator
+	tbl.cells = make([dynamic]Cell, tbl.table_allocator)
+	tbl.colw = make([dynamic]int, tbl.table_allocator)
+	tbl.format_allocator = format_allocator
+	return tbl
+}
+init_with_virtual_arena :: proc(tbl: ^Table, format_arena: ^virtual.Arena, table_allocator := context.allocator) -> ^Table {
+	return init_with_allocator(tbl, virtual.arena_allocator(format_arena), table_allocator)
+}
+init_with_mem_arena :: proc(tbl: ^Table, format_arena: ^mem.Arena, table_allocator := context.allocator) -> ^Table {
+	return init_with_allocator(tbl, mem.arena_allocator(format_arena), table_allocator)
+}
+
+destroy :: proc(tbl: ^Table) {
+	free_all(tbl.format_allocator)
+	delete(tbl.cells)
+	delete(tbl.colw)
+}
+
+caption :: proc(tbl: ^Table, value: string) {
+	tbl.caption = value
+	tbl.dirty = true
+}
+
+padding :: proc(tbl: ^Table, lpad, rpad: int) {
+	tbl.lpad = lpad
+	tbl.rpad = rpad
+	tbl.dirty = true
+}
+
+get_cell :: proc(tbl: ^Table, row, col: int, loc := #caller_location) -> ^Cell {
+	assert(col >= 0 && col < tbl.nr_cols, "cell column out of range", loc)
+	assert(row >= 0 && row < tbl.nr_rows, "cell row out of range", loc)
+	resize(&tbl.cells, tbl.nr_cols * tbl.nr_rows)
+	return &tbl.cells[row*tbl.nr_cols + col]
+}
+
+set_cell_value_and_alignment :: proc(tbl: ^Table, row, col: int, value: string, alignment: Cell_Alignment) {
+	cell := get_cell(tbl, row, col)
+	cell.text = format(tbl, "%v", value)
+	cell.alignment = alignment
+	tbl.dirty = true
+}
+
+set_cell_value :: proc(tbl: ^Table, row, col: int, value: any, loc := #caller_location) {
+	cell := get_cell(tbl, row, col, loc)
+	switch val in value {
+	case nil:
+		cell.text = ""
+	case string:
+		cell.text = string(val)
+	case cstring:
+		cell.text = string(val)
+	case:
+		cell.text = format(tbl, "%v", val)
+		if cell.text == "" {
+			fmt.eprintf("{} text/table: format() resulted in empty string (arena out of memory?)\n", loc)
+		}
+	}
+	tbl.dirty = true
+}
+
+set_cell_alignment :: proc(tbl: ^Table, row, col: int, alignment: Cell_Alignment, loc := #caller_location) {
+	cell := get_cell(tbl, row, col, loc)
+	cell.alignment = alignment
+	tbl.dirty = true
+}
+
+format :: proc(tbl: ^Table, _fmt: string, args: ..any, loc := #caller_location) -> string {
+	context.allocator = tbl.format_allocator
+	return fmt.aprintf(fmt = _fmt, args = args)
+}
+
+header :: proc(tbl: ^Table, values: ..any, loc := #caller_location) {
+	if (tbl.has_header_row && tbl.nr_rows != 1) || (!tbl.has_header_row && tbl.nr_rows != 0) {
+		panic("Cannot add headers after rows have been added", loc)
+	}
+
+	if tbl.nr_rows == 0 {
+		tbl.nr_rows += 1
+		tbl.has_header_row = true
+	}
+
+	col := tbl.nr_cols
+	tbl.nr_cols += len(values)
+	for val in values {
+		set_cell_value(tbl, header_row(tbl), col, val, loc)
+		col += 1
+	}
+
+	tbl.dirty = true
+}
+
+row :: proc(tbl: ^Table, values: ..any, loc := #caller_location) {
+	if tbl.nr_cols == 0 {
+		if len(values) == 0 {
+			panic("Cannot create row without values unless knowing amount of columns in advance")
+		} else {
+			tbl.nr_cols = len(values)
+		}
+	}
+	tbl.nr_rows += 1
+	for col in 0..<tbl.nr_cols {
+		val := values[col] if col < len(values) else nil
+		set_cell_value(tbl, last_row(tbl), col, val)
+	}
+	tbl.dirty = true
+}
+
+last_row :: proc(tbl: ^Table) -> int {
+	return tbl.nr_rows - 1
+}
+
+header_row :: proc(tbl: ^Table) -> int {
+	return 0 if tbl.has_header_row else -1
+}
+
+first_row :: proc(tbl: ^Table) -> int {
+	return header_row(tbl)+1 if tbl.has_header_row else 0
+}
+
+build :: proc(tbl: ^Table) {
+	tbl.dirty = false
+
+	resize(&tbl.colw, tbl.nr_cols)
+	mem.zero_slice(tbl.colw[:])
+
+	for row in 0..<tbl.nr_rows {
+		for col in 0..<tbl.nr_cols {
+			cell := get_cell(tbl, row, col)
+			if w := len(cell.text) + tbl.lpad + tbl.rpad; w > tbl.colw[col] {
+				tbl.colw[col] = w
+			}
+		}
+	}
+
+	colw_sum := 0
+	for v in tbl.colw {
+		colw_sum += v
+	}
+
+	tbl.tblw = max(colw_sum, len(tbl.caption) + tbl.lpad + tbl.rpad)
+
+	// Resize columns to match total width of table
+	remain := tbl.tblw-colw_sum
+	for col := 0; remain > 0; col = (col + 1) % tbl.nr_cols {
+		tbl.colw[col] += 1
+		remain -= 1
+	}
+
+	return
+}
+
+write_html_table :: proc(w: io.Writer, tbl: ^Table) {
+	if tbl.dirty {
+		build(tbl)
+	}
+
+	io.write_string(w, "<table>\n")
+	if tbl.caption != "" {
+		io.write_string(w, "<caption>")
+		io.write_string(w, tbl.caption)
+		io.write_string(w, "</caption>\n")
+	}
+
+	align_attribute :: proc(cell: ^Cell) -> string {
+		switch cell.alignment {
+		case .Left:   return ` align="left"`
+		case .Center: return ` align="center"`
+		case .Right:  return ` align="right"`
+		}
+		unreachable()
+	}
+
+	if tbl.has_header_row {
+		io.write_string(w, "<thead>\n")
+		io.write_string(w, "  <tr>\n")
+		for col in 0..<tbl.nr_cols {
+			cell := get_cell(tbl, header_row(tbl), col)
+			io.write_string(w, "    <th")
+			io.write_string(w, align_attribute(cell))
+			io.write_string(w, ">")
+			io.write_string(w, cell.text)
+			io.write_string(w, "</th>\n")
+		}
+		io.write_string(w, "  </tr>\n")
+		io.write_string(w, "</thead>\n")
+	}
+
+	io.write_string(w, "<tbody>\n")
+	for row in 0..<tbl.nr_rows {
+		if tbl.has_header_row && row == header_row(tbl) {
+			continue
+		}
+		io.write_string(w, "  <tr>\n")
+		for col in 0..<tbl.nr_cols {
+			cell := get_cell(tbl, row, col)
+			io.write_string(w, "    <td")
+			io.write_string(w, align_attribute(cell))
+			io.write_string(w, ">")
+			io.write_string(w, cell.text)
+			io.write_string(w, "</td>\n")
+		}
+		io.write_string(w, "  </tr>\n")
+	}
+	io.write_string(w, "  </tbody>\n")
+
+	io.write_string(w, "</table>\n")
+}
+
+write_ascii_table :: proc(w: io.Writer, tbl: ^Table) {
+	if tbl.dirty {
+		build(tbl)
+	}
+
+	write_caption_separator :: proc(w: io.Writer, tbl: ^Table) {
+		io.write_byte(w, '+')
+		write_byte_repeat(w, tbl.tblw + tbl.nr_cols - 1, '-')
+		io.write_byte(w, '+')
+		io.write_byte(w, '\n')
+	}
+
+	write_table_separator :: proc(w: io.Writer, tbl: ^Table) {
+		for col in 0..<tbl.nr_cols {
+			if col == 0 {
+				io.write_byte(w, '+')
+			}
+			write_byte_repeat(w, tbl.colw[col], '-')
+			io.write_byte(w, '+')
+		}
+		io.write_byte(w, '\n')
+	}
+
+	if tbl.caption != "" {
+		write_caption_separator(w, tbl)
+		io.write_byte(w, '|')
+		write_text_align(w, tbl.tblw -  tbl.lpad - tbl.rpad + tbl.nr_cols - 1,
+		                 tbl.lpad, tbl.rpad, tbl.caption, .Center)
+		io.write_byte(w, '|')
+		io.write_byte(w, '\n')
+	}
+
+	write_table_separator(w, tbl)
+	for row in 0..<tbl.nr_rows {
+		for col in 0..<tbl.nr_cols {
+			if col == 0 {
+				io.write_byte(w, '|')
+			}
+			write_table_cell(w, tbl, row, col)
+			io.write_byte(w, '|')
+		}
+		io.write_byte(w, '\n')
+		if tbl.has_header_row && row == header_row(tbl) {
+			write_table_separator(w, tbl)
+		}
+	}
+	write_table_separator(w, tbl)
+}
+
+// Renders table according to GitHub Flavored Markdown (GFM) specification
+write_markdown_table :: proc(w: io.Writer, tbl: ^Table) {
+	// NOTE(oskar): Captions or colspans are not supported by GFM as far as I can tell.
+
+	if tbl.dirty {
+		build(tbl)
+	}
+
+	for row in 0..<tbl.nr_rows {
+		for col in 0..<tbl.nr_cols {
+			cell := get_cell(tbl, row, col)
+			if col == 0 {
+				io.write_byte(w, '|')
+			}
+			write_text_align(w, tbl.colw[col] - tbl.lpad - tbl.rpad, tbl.lpad, tbl.rpad, cell.text,
+			                 .Center if tbl.has_header_row && row == header_row(tbl) else .Left)
+			io.write_string(w, "|")
+		}
+		io.write_byte(w, '\n')
+
+		if tbl.has_header_row && row == header_row(tbl) {
+			for col in 0..<tbl.nr_cols {
+				cell := get_cell(tbl, row, col)
+				if col == 0 {
+					io.write_byte(w, '|')
+				}
+				switch cell.alignment {
+				case .Left:
+					io.write_byte(w, ':')
+					write_byte_repeat(w, max(1, tbl.colw[col]-1), '-')
+				case .Center:
+					io.write_byte(w, ':')
+					write_byte_repeat(w, max(1, tbl.colw[col]-2), '-')
+					io.write_byte(w, ':')
+				case .Right:
+					write_byte_repeat(w, max(1, tbl.colw[col]-1), '-')
+					io.write_byte(w, ':')
+				}
+				io.write_byte(w, '|')
+			}
+			io.write_byte(w, '\n')
+		}
+	}
+}
+
+write_byte_repeat :: proc(w: io.Writer, n: int, b: byte) {
+	for _ in 0..<n {
+		io.write_byte(w, b)
+	}
+}
+
+write_table_cell :: proc(w: io.Writer, tbl: ^Table, row, col: int) {
+	if tbl.dirty {
+		build(tbl)
+	}
+	cell := get_cell(tbl, row, col)
+	write_text_align(w, tbl.colw[col]-tbl.lpad-tbl.rpad, tbl.lpad, tbl.rpad, cell.text, cell.alignment)
+}
+
+write_text_align :: proc(w: io.Writer, colw, lpad, rpad: int, text: string, alignment: Cell_Alignment) {
+	write_byte_repeat(w, lpad, ' ')
+	switch alignment {
+	case .Left:
+		io.write_string(w, text)
+		write_byte_repeat(w, colw - len(text), ' ')
+	case .Center:
+		pad := colw - len(text)
+		odd := pad & 1 != 0
+		write_byte_repeat(w, pad/2, ' ')
+		io.write_string(w, text)
+		write_byte_repeat(w, pad/2 + 1 if odd else pad/2, ' ')
+	case .Right:
+		write_byte_repeat(w, colw - len(text), ' ')
+		io.write_string(w, text)
+	}
+	write_byte_repeat(w, rpad, ' ')
+}

+ 13 - 0
core/text/table/utility.odin

@@ -0,0 +1,13 @@
+package text_table
+
+import "core:io"
+import "core:os"
+import "core:strings"
+
+stdio_writer :: proc() -> io.Writer {
+	return io.to_writer(os.stream_from_handle(os.stdout))
+}
+
+strings_builder_writer :: proc(b: ^strings.Builder) -> io.Writer {
+	return strings.to_writer(b)
+}

+ 3 - 3
src/main.cpp

@@ -594,13 +594,13 @@ gb_internal Array<String> setup_args(int argc, char const **argv) {
 
 gb_internal void print_usage_line(i32 indent, char const *fmt, ...) {
 	while (indent --> 0) {
-		gb_printf_err("\t");
+		gb_printf("\t");
 	}
 	va_list va;
 	va_start(va, fmt);
-	gb_printf_err_va(fmt, va);
+	gb_printf_va(fmt, va);
 	va_end(va);
-	gb_printf_err("\n");
+	gb_printf("\n");
 }
 
 gb_internal void usage(String argv0) {

+ 5 - 3
src/parser.cpp

@@ -3693,9 +3693,11 @@ gb_internal bool allow_field_separator(AstFile *f) {
 	if (allow_token(f, Token_Comma)) {
 		return true;
 	}
-	if (ALLOW_NEWLINE && token.kind == Token_Semicolon && !token_is_newline(token)) {
-		String p = token_to_string(token);
-		syntax_error(token_end_of_line(f, f->prev_token), "Expected a comma, got a %.*s", LIT(p));
+	if (ALLOW_NEWLINE && token.kind == Token_Semicolon) {
+		if (!token_is_newline(token)) {
+			String p = token_to_string(token);
+			syntax_error(token_end_of_line(f, f->prev_token), "Expected a comma, got a %.*s", LIT(p));
+		}
 		advance_token(f);
 		return true;
 	}

+ 19 - 24
tests/core/build.bat

@@ -6,87 +6,82 @@ python3 download_assets.py
 echo ---
 echo Running core:image tests
 echo ---
-%PATH_TO_ODIN% run image    %COMMON% -out:test_core_image.exe
+%PATH_TO_ODIN% run image    %COMMON% -out:test_core_image.exe || exit /b
 
 echo ---
 echo Running core:compress tests
 echo ---
-%PATH_TO_ODIN% run compress %COMMON% -out:test_core_compress.exe
+%PATH_TO_ODIN% run compress %COMMON% -out:test_core_compress.exe || exit /b
 
 echo ---
 echo Running core:strings tests
 echo ---
-%PATH_TO_ODIN% run strings %COMMON% -out:test_core_strings.exe
+%PATH_TO_ODIN% run strings %COMMON% -out:test_core_strings.exe || exit /b
 
 echo ---
 echo Running core:hash tests
 echo ---
-%PATH_TO_ODIN% run hash %COMMON% -o:size -out:test_core_hash.exe
+%PATH_TO_ODIN% run hash %COMMON% -o:size -out:test_core_hash.exe || exit /b
 
 echo ---
 echo Running core:odin tests
 echo ---
-%PATH_TO_ODIN% run odin %COMMON% -o:size -out:test_core_odin.exe
+%PATH_TO_ODIN% run odin %COMMON% -o:size -out:test_core_odin.exe || exit /b
 
 echo ---
 echo Running core:crypto hash tests
 echo ---
-%PATH_TO_ODIN% run crypto %COMMON% -out:test_crypto_hash.exe
+%PATH_TO_ODIN% run crypto %COMMON% -out:test_crypto_hash.exe || exit /b
 
 echo ---
 echo Running core:encoding tests
 echo ---
-%PATH_TO_ODIN% run encoding/hxa    %COMMON% %COLLECTION% -out:test_hxa.exe
-%PATH_TO_ODIN% run encoding/json   %COMMON% -out:test_json.exe
-%PATH_TO_ODIN% run encoding/varint %COMMON% -out:test_varint.exe
-%PATH_TO_ODIN% run encoding/xml    %COMMON% -out:test_xml.exe
+%PATH_TO_ODIN% run encoding/hxa    %COMMON% %COLLECTION% -out:test_hxa.exe || exit /b
+%PATH_TO_ODIN% run encoding/json   %COMMON% -out:test_json.exe || exit /b
+%PATH_TO_ODIN% run encoding/varint %COMMON% -out:test_varint.exe || exit /b
+%PATH_TO_ODIN% run encoding/xml    %COMMON% -out:test_xml.exe || exit /b
 
 echo ---
 echo Running core:math/noise tests
 echo ---
-%PATH_TO_ODIN% run math/noise %COMMON% -out:test_noise.exe
+%PATH_TO_ODIN% run math/noise %COMMON% -out:test_noise.exe || exit /b
 
 echo ---
 echo Running core:math tests
 echo ---
-%PATH_TO_ODIN% run math %COMMON% %COLLECTION% -out:test_core_math.exe
+%PATH_TO_ODIN% run math %COMMON% %COLLECTION% -out:test_core_math.exe || exit /b
 
 echo ---
 echo Running core:math/linalg/glsl tests
 echo ---
-%PATH_TO_ODIN% run math/linalg/glsl %COMMON% %COLLECTION% -out:test_linalg_glsl.exe
+%PATH_TO_ODIN% run math/linalg/glsl %COMMON% %COLLECTION% -out:test_linalg_glsl.exe || exit /b
 
 echo ---
 echo Running core:path/filepath tests
 echo ---
-%PATH_TO_ODIN% run path/filepath %COMMON% %COLLECTION% -out:test_core_filepath.exe
+%PATH_TO_ODIN% run path/filepath %COMMON% %COLLECTION% -out:test_core_filepath.exe || exit /b
 
 echo ---
 echo Running core:reflect tests
 echo ---
-%PATH_TO_ODIN% run reflect %COMMON% %COLLECTION% -out:test_core_reflect.exe
+%PATH_TO_ODIN% run reflect %COMMON% %COLLECTION% -out:test_core_reflect.exe || exit /b
 
 echo ---
 echo Running core:text/i18n tests
 echo ---
-%PATH_TO_ODIN% run text\i18n %COMMON% -out:test_core_i18n.exe
+%PATH_TO_ODIN% run text\i18n %COMMON% -out:test_core_i18n.exe || exit /b
 
 echo ---
 echo Running core:net
 echo ---
-%PATH_TO_ODIN% run net %COMMON% -out:test_core_net.exe
-
-echo ---
-echo Running core:text/lua tests
-echo ---
-%PATH_TO_ODIN% run text\lua %COMMON% -out:test_core_lua_strlib.exe
+%PATH_TO_ODIN% run net %COMMON% -out:test_core_net.exe || exit /b
 
 echo ---
 echo Running core:slice tests
 echo ---
-%PATH_TO_ODIN% run slice %COMMON% -out:test_core_slice.exe
+%PATH_TO_ODIN% run slice %COMMON% -out:test_core_slice.exe || exit /b
 
 echo ---
 echo Running core:container tests
 echo ---
-%PATH_TO_ODIN% run container %COMMON% %COLLECTION% -out:test_core_container.exe
+%PATH_TO_ODIN% run container %COMMON% %COLLECTION% -out:test_core_container.exe || exit /b

+ 7 - 0
tests/core/compress/test_core_compress.odin

@@ -151,6 +151,13 @@ shoco_test :: proc(t: ^testing.T) {
 	}
 
 	for v in Shoco_Tests {
+		when ODIN_OS == .Windows {
+			v := v
+			// Compressed source files are not encoded with carriage returns but git replaces raw files lf with crlf on commit (on windows only)
+			// So replace crlf with lf on windows
+			v.raw, _ = bytes.replace_all(v.raw, { 0xD, 0xA }, { 0xA })
+		}
+
 		expected_raw        := len(v.raw)
 		expected_compressed := len(v.compressed)
 

+ 13 - 0
tests/documentation/build.bat

@@ -0,0 +1,13 @@
+@echo off
+set PATH_TO_ODIN==..\..\odin
+
+echo ---
+echo Building Documentation File
+echo ---
+%PATH_TO_ODIN% doc ..\..\examples\all -all-packages -doc-format || exit /b
+
+
+echo ---
+echo Running Documentation Tester
+echo ---
+%PATH_TO_ODIN% run documentation_tester.odin -file -vet -strict-style -- %PATH_TO_ODIN% || exit /b

+ 421 - 0
tests/documentation/documentation_tester.odin

@@ -0,0 +1,421 @@
+package documentation_tester
+
+import "core:os"
+import "core:io"
+import "core:fmt"
+import "core:strings"
+import "core:odin/ast"
+import "core:odin/parser"
+import "core:c/libc"
+import doc "core:odin/doc-format"
+
+Example_Test :: struct {
+	entity_name: string,
+	package_name: string,
+	example_code: []string,
+	expected_output: []string,
+}
+
+g_header:   ^doc.Header
+g_bad_doc: bool
+g_examples_to_verify: [dynamic]Example_Test
+g_path_to_odin: string
+
+array :: proc(a: $A/doc.Array($T)) -> []T {
+	return doc.from_array(g_header, a)
+}
+
+str :: proc(s: $A/doc.String) -> string {
+	return doc.from_string(g_header, s)
+}
+
+common_prefix :: proc(strs: []string) -> string {
+	if len(strs) == 0 {
+		return ""
+	}
+	n := max(int)
+	for str in strs {
+		n = min(n, len(str))
+	}
+
+	prefix := strs[0][:n]
+	for str in strs[1:] {
+		for len(prefix) != 0 && str[:len(prefix)] != prefix {
+			prefix = prefix[:len(prefix)-1]
+		}
+		if len(prefix) == 0 {
+			break
+		}
+	}
+	return prefix
+}
+
+errorf :: proc(format: string, args: ..any) -> ! {
+	fmt.eprintf("%s ", os.args[0])
+	fmt.eprintf(format, ..args)
+	fmt.eprintln()
+	os.exit(1)
+}
+
+main :: proc() {
+	if len(os.args) != 2 {
+		errorf("expected path to odin executable")
+	}
+	g_path_to_odin = os.args[1]
+	data, ok := os.read_entire_file("all.odin-doc")
+	if !ok {
+		errorf("unable to read file: all.odin-doc")
+	}
+	err: doc.Reader_Error
+	g_header, err = doc.read_from_bytes(data)
+	switch err {
+	case .None:
+	case .Header_Too_Small:
+		errorf("file is too small for the file format")
+	case .Invalid_Magic:
+		errorf("invalid magic for the file format")
+	case .Data_Too_Small:
+		errorf("data is too small for the file format")
+	case .Invalid_Version:
+		errorf("invalid file format version")
+	}
+	pkgs     := array(g_header.pkgs)
+	entities := array(g_header.entities)
+
+	path_prefix: string
+	{
+		fullpaths: [dynamic]string
+		defer delete(fullpaths)
+
+		for pkg in pkgs[1:] {
+			append(&fullpaths, str(pkg.fullpath))
+		}
+		path_prefix = common_prefix(fullpaths[:])
+	}
+
+	for pkg in pkgs[1:] {
+		entries_array := array(pkg.entries)
+		fullpath := str(pkg.fullpath)
+		path := strings.trim_prefix(fullpath, path_prefix)
+		if ! strings.has_prefix(path, "core/") {
+			continue
+		}
+		trimmed_path := strings.trim_prefix(path, "core/")
+		if strings.has_prefix(trimmed_path, "sys") {
+			continue
+		}
+		if strings.contains(trimmed_path, "/_") {
+			continue
+		}
+		for entry in entries_array {
+			entity := entities[entry.entity]
+			find_and_add_examples(
+				docs = str(entity.docs),
+				package_name = str(pkg.name),
+				entity_name = str(entity.name),
+			)
+		}
+	}
+	write_test_suite(g_examples_to_verify[:])
+	if g_bad_doc {
+		errorf("We created bad documentation!")
+	}
+
+	if ! run_test_suite() {
+		errorf("Test suite failed!")
+	}
+	fmt.println("Examples verified")
+}
+
+// NOTE: this is a pretty close copy paste from the website pkg documentation on parsing the docs
+find_and_add_examples :: proc(docs: string, package_name: string, entity_name: string) {
+	if docs == "" {
+		return
+	}
+	Block_Kind :: enum {
+		Other,
+		Example,
+		Output,
+	}
+	Block :: struct {
+		kind: Block_Kind,
+		lines: []string,
+	}
+	lines := strings.split_lines(docs)
+	curr_block_kind := Block_Kind.Other
+	start := 0
+
+	example_block: Block // when set the kind should be Example
+	output_block: Block // when set the kind should be Output
+	// rely on zii that the kinds have not been set
+	assert(example_block.kind != .Example)
+	assert(output_block.kind != .Output)
+
+	insert_block :: proc(block: Block, example: ^Block, output: ^Block, name: string) {
+		switch block.kind {
+		case .Other:
+		case .Example:
+			if example.kind == .Example {
+				fmt.eprintf("The documentation for %q has multiple examples which is not allowed\n", name)
+				g_bad_doc = true
+			}
+			example^ = block
+		case .Output: output^ = block
+			if example.kind == .Output {
+				fmt.eprintf("The documentation for %q has multiple output which is not allowed\n", name)
+				g_bad_doc = true
+			}
+			output^ = block
+		}
+	}
+
+	for line, i in lines {
+		text := strings.trim_space(line)
+		next_block_kind := curr_block_kind
+
+		switch curr_block_kind {
+		case .Other:
+			switch {
+			case strings.has_prefix(line, "Example:"): next_block_kind = .Example
+			case strings.has_prefix(line, "Output:"): next_block_kind = .Output
+			}
+		case .Example:
+			switch {
+			case strings.has_prefix(line, "Output:"): next_block_kind = .Output
+			case ! (text == "" || strings.has_prefix(line, "\t")): next_block_kind = .Other
+			}
+		case .Output:
+			switch {
+			case strings.has_prefix(line, "Example:"): next_block_kind = .Example
+			case ! (text == "" || strings.has_prefix(line, "\t")): next_block_kind = .Other
+			}
+		}
+
+		if i-start > 0 && (curr_block_kind != next_block_kind) {
+			insert_block(Block{curr_block_kind, lines[start:i]}, &example_block, &output_block, entity_name)
+			curr_block_kind, start = next_block_kind, i
+		}
+	}
+
+	if start < len(lines) {
+		insert_block(Block{curr_block_kind, lines[start:]}, &example_block, &output_block, entity_name)
+	}
+
+	if output_block.kind == .Output && example_block.kind != .Example {
+		fmt.eprintf("The documentation for %q has an output block but no example\n", entity_name)
+		g_bad_doc = true
+	}
+
+	// Write example and output block if they're both present
+	if example_block.kind == .Example && output_block.kind == .Output {
+		{
+			// Example block starts with
+			// `Example:` and a number of white spaces,
+			lines := &example_block.lines
+			for len(lines) > 0 && (strings.trim_space(lines[0]) == "" || strings.has_prefix(lines[0], "Example:")) {
+				lines^ = lines[1:]
+			}
+		}
+		{
+			// Output block starts with
+			// `Output:` and a number of white spaces,
+			lines := &output_block.lines
+			for len(lines) > 0 && (strings.trim_space(lines[0]) == "" || strings.has_prefix(lines[0], "Output:")) {
+				lines^ = lines[1:]
+			}
+			// Additionally we need to strip all empty lines at the end of output to not include those in the expected output
+			for len(lines) > 0 && (strings.trim_space(lines[len(lines) - 1]) == "") {
+				lines^ = lines[:len(lines) - 1]
+			}
+		}
+		// Remove first layer of tabs which are always present
+		for line in &example_block.lines {
+			line = strings.trim_prefix(line, "\t")
+		}
+		for line in &output_block.lines {
+			line = strings.trim_prefix(line, "\t")
+		}
+		append(&g_examples_to_verify, Example_Test {
+			entity_name = entity_name,
+			package_name = package_name,
+			example_code = example_block.lines,
+			expected_output = output_block.lines,
+		})
+	}
+}
+
+
+write_test_suite :: proc(example_tests: []Example_Test) {
+	TEST_SUITE_DIRECTORY :: "verify"
+	os.remove_directory(TEST_SUITE_DIRECTORY)
+	os.make_directory(TEST_SUITE_DIRECTORY)
+
+	example_build := strings.builder_make()
+	test_runner := strings.builder_make()
+
+	strings.write_string(&test_runner,
+`//+private
+package documentation_verification
+
+import "core:os"
+import "core:mem"
+import "core:io"
+import "core:fmt"
+import "core:thread"
+import "core:sync"
+import "core:intrinsics"
+
+@(private="file")
+_read_pipe: os.Handle
+@(private="file")
+_write_pipe: os.Handle
+@(private="file")
+_pipe_reader_semaphore: sync.Sema
+@(private="file")
+_out_data: string
+@(private="file")
+_out_buffer: [mem.Megabyte]byte
+@(private="file")
+_bad_test_found: bool
+
+@(private="file")
+_spawn_pipe_reader :: proc() {
+	thread.create_and_start(proc(^thread.Thread) {
+		stream := os.stream_from_handle(_read_pipe)
+		reader := io.to_reader(stream)
+		sync.post(&_pipe_reader_semaphore) // notify thread is ready
+		for {
+			n_read := 0
+			read_to_null_byte := 0
+			finished_reading := false
+			for ! finished_reading {
+				just_read, err := io.read(reader, _out_buffer[n_read:], &n_read); if err != .None {
+					panic("We got an IO error!")
+				}
+				for b in _out_buffer[n_read - just_read: n_read] {
+					if b == 0 {
+						finished_reading = true
+						break
+					}
+				read_to_null_byte += 1
+				}
+			}
+			intrinsics.volatile_store(&_out_data, transmute(string)_out_buffer[:read_to_null_byte])
+			sync.post(&_pipe_reader_semaphore) // notify we read the null byte
+		}
+	})
+
+	sync.wait(&_pipe_reader_semaphore) // wait for thread to be ready
+}
+
+@(private="file")
+_check :: proc(test_name: string, expected: string) {
+	null_byte: [1]byte
+	os.write(_write_pipe, null_byte[:])
+	os.flush(_write_pipe)
+	sync.wait(&_pipe_reader_semaphore)
+	output := intrinsics.volatile_load(&_out_data) // wait for thread to read null byte
+	if expected != output {
+		fmt.eprintf("Test %q got unexpected output:\n%q\n", test_name, output)
+		fmt.eprintf("Expected:\n%q\n", expected)
+		_bad_test_found = true
+	}
+}
+
+main :: proc() {
+	_read_pipe, _write_pipe, _ = os.pipe()
+	os.stdout = _write_pipe
+	_spawn_pipe_reader()
+`)
+	for test in example_tests {
+		strings.builder_reset(&example_build)
+		strings.write_string(&example_build, "package documentation_verification\n\n")
+		for line in test.example_code {
+			strings.write_string(&example_build, line)
+			strings.write_byte(&example_build, '\n')
+		}
+
+		code_string := strings.to_string(example_build)
+
+		example_ast := ast.File { src = code_string }
+		odin_parser := parser.default_parser()
+
+		if ! parser.parse_file(&odin_parser, &example_ast) {
+			g_bad_doc = true
+			continue
+		}
+		if odin_parser.error_count > 0 {
+			fmt.eprintf("Errors on the following code generated for %q:\n%v\n", test.entity_name, code_string)
+			g_bad_doc = true
+			continue
+		}
+
+		enforced_name := fmt.tprintf("%v_example", test.entity_name)
+		index_of_proc_name: int
+		code_test_name: string
+
+		for d in example_ast.decls {
+			value_decl, is_value := d.derived.(^ast.Value_Decl); if ! is_value {
+				continue
+			}
+			if len(value_decl.values) != 1 {
+				continue
+			}
+			proc_lit, is_proc_lit := value_decl.values[0].derived_expr.(^ast.Proc_Lit); if ! is_proc_lit {
+				continue
+			}
+			if len(proc_lit.type.params.list) > 0 {
+				continue
+			}
+			this_procedure_name := code_string[value_decl.names[0].pos.offset:value_decl.names[0].end.offset]
+			if this_procedure_name != enforced_name {
+				continue
+			}
+			index_of_proc_name = value_decl.names[0].pos.offset
+			code_test_name = this_procedure_name
+			break
+		}
+
+		if code_test_name == "" {
+			fmt.eprintf("We could not any find procedure literals with no arguments with the identifier %q for the example for %q\n", enforced_name, test.entity_name)
+			g_bad_doc = true
+			continue
+		}
+
+		fmt.sbprintf(&test_runner, "\t%v_%v()\n", test.package_name, code_test_name)
+		fmt.sbprintf(&test_runner, "\t_check(%q, `", code_test_name)
+		for line in test.expected_output {
+			strings.write_string(&test_runner, line)
+			strings.write_string(&test_runner, "\n")
+		}
+		strings.write_string(&test_runner, "`)\n")
+		save_path := fmt.tprintf("verify/test_%v_%v.odin", test.package_name, code_test_name)
+
+		test_file_handle, err := os.open(save_path, os.O_WRONLY | os.O_CREATE); if err != 0 {
+			fmt.eprintf("We could not open the file to the path %q for writing\n", save_path)
+			g_bad_doc = true
+			continue
+		}
+		defer os.close(test_file_handle)
+		stream := os.stream_from_handle(test_file_handle)
+		writer, ok := io.to_writer(stream); if ! ok {
+			fmt.eprintf("We could not make the writer for the path %q\n", save_path)
+			g_bad_doc = true
+			continue
+		}
+		fmt.wprintf(writer, "%v%v_%v", code_string[:index_of_proc_name], test.package_name, code_string[index_of_proc_name:])
+	}
+
+	strings.write_string(&test_runner,
+`
+	if _bad_test_found {
+		fmt.eprintln("One or more tests failed")
+		os.exit(1)
+	}
+}`)
+	os.write_entire_file("verify/main.odin", transmute([]byte)strings.to_string(test_runner))
+}
+
+run_test_suite :: proc() -> bool {
+	return libc.system(fmt.caprintf("%v run verify", g_path_to_odin)) == 0
+}

+ 1 - 1
tests/internal/build.bat

@@ -1,4 +1,4 @@
 @echo off
 set PATH_TO_ODIN==..\..\odin
-%PATH_TO_ODIN% run test_map.odin -file -vet -strict-style -o:minimal
+%PATH_TO_ODIN% run test_map.odin -file -vet -strict-style -o:minimal || exit /b
 rem -define:SEED=42

+ 4 - 9
tests/issues/run.bat

@@ -5,19 +5,14 @@ pushd build
 
 set COMMON=-collection:tests=..\..
 
-set ERROR_DID_OCCUR=0
-
 @echo on
 
-..\..\..\odin test ..\test_issue_829.odin %COMMON% -file
-..\..\..\odin test ..\test_issue_1592.odin %COMMON% -file
-..\..\..\odin test ..\test_issue_2087.odin %COMMON% -file
-..\..\..\odin build ..\test_issue_2113.odin %COMMON% -file -debug
+..\..\..\odin test ..\test_issue_829.odin %COMMON% -file || exit /b
+..\..\..\odin test ..\test_issue_1592.odin %COMMON% -file || exit /b
+..\..\..\odin test ..\test_issue_2087.odin %COMMON% -file || exit /b
+..\..\..\odin build ..\test_issue_2113.odin %COMMON% -file -debug || exit /b
 
 @echo off
 
-if %ERRORLEVEL% NEQ 0 set ERROR_DID_OCCUR=1
-
 popd
 rmdir /S /Q build
-if %ERROR_DID_OCCUR% NEQ 0 EXIT /B 1

+ 2 - 2
tests/vendor/build.bat

@@ -5,9 +5,9 @@ set PATH_TO_ODIN==..\..\odin
 echo ---
 echo Running vendor:botan tests
 echo ---
-%PATH_TO_ODIN% run botan %COMMON% -out:vendor_botan.exe
+%PATH_TO_ODIN% run botan %COMMON% -out:vendor_botan.exe || exit /b
 
 echo ---
 echo Running vendor:glfw tests
 echo ---
-%PATH_TO_ODIN% run glfw %COMMON% -out:vendor_glfw.exe
+%PATH_TO_ODIN% run glfw %COMMON% -out:vendor_glfw.exe || exit /b

Some files were not shown because too many files changed in this diff