.{Kind, string, unsigned_int, signed_int, float, integer, memory, panic} := @use("stn") format_int := fn($T: type, v: T, str: ^u8, radix: T): uint { if integer(T) { is_negative := signed_int(T) & v < 0 if is_negative v = -v prefix_len := 0 if radix == 16 { *str = '0'; *(str + 1) = 'x' prefix_len = 2 } else if radix == 2 { *str = '0'; *(str + 1) = 'b' prefix_len = 2 } else if radix == 8 { *str = '0'; *(str + 1) = 'o' prefix_len = 2 } if v == 0 { *(str + prefix_len) = '0' return prefix_len + 1 } i := 0 loop if v <= 0 break else { remainder := v % radix v /= radix if remainder > 9 { *(str + prefix_len + i) = @intcast(remainder - 10 + 'A') } else { *(str + prefix_len + i) = @intcast(remainder + '0') } i += 1 } if is_negative { *(str + prefix_len + i) = '-' i += 1 } @inline(string.reverse, str + prefix_len) return prefix_len + i } else { panic("Type is not an integer\0") } } format_bool := fn(v: bool, str: ^u8): uint { if v { *@as(^[u8; 4], @bitcast(str)) = *@bitcast("true\0") return 4 } else { *@as(^[u8; 5], @bitcast(str)) = *@bitcast("false\0") return 5 } } format_float := fn($T: type, v: T, str: ^u8, precision: uint, radix: int): uint { if float(T) { is_negative := v < 0 i := 0 prefix_len := 0 if radix == 16 { *str = '0'; *(str + 1) = 'x' prefix_len = 2 } else if radix == 2 { *str = '0'; *(str + 1) = 'b' prefix_len = 2 } else if radix == 8 { *str = '0'; *(str + 1) = 'o' prefix_len = 2 } if is_negative v = -v integer_part := @fti(v) fractional_part := v - @itf(integer_part) loop if integer_part == 0 & i > 0 break else { remainder := integer_part % radix integer_part /= radix if remainder > 9 { *(str + prefix_len + i) = @intcast(remainder - 10 + 'A') } else { *(str + prefix_len + i) = @intcast(remainder + '0') } i += 1 } if is_negative { *(str + prefix_len + i) = '-' i += 1 } @inline(string.reverse, str + prefix_len); *(str + prefix_len + i) = '.' i += 1 p := precision tolerance := @as(T, 0.00000001) loop if p <= 0 | fractional_part < tolerance break else { fractional_part *= @itf(radix) digit := @fti(fractional_part) if digit > 9 { *(str + prefix_len + i) = @intcast(digit - 10 + 'A') } else { *(str + prefix_len + i) = @intcast(digit + '0') } i += 1 fractional_part -= @itf(digit) p -= 1 } return prefix_len + i } else { panic("Type is not a floating point\0") } } format_inner := fn($T: type, v: T, str: ^u8, opts: FormatOptions): uint { match @as(Kind, @bitcast(@kindof(T))) { .Pointer => return @inline(format_int, uint, @bitcast(v), str, 16), .Builtin => { if integer(T) { return @inline(format_int, T, v, str, @intcast(opts.radix)) } else if T == bool { return @inline(format_bool, v, str) } else if float(T) { return @inline(format_float, T, v, str, opts.decimal_digits, @intcast(opts.radix)) } }, .Struct => { // name := ; i := 0; *@as(^[u8; @len(@nameof(T))], @bitcast(str)) = *@bitcast(@nameof(T)) len := @len(@nameof(T)); *@as(^[u8; 2], @bitcast(str + len)) = *@bitcast(".(\0") len += 2 $loop { v_sub := v[i] TSub := @TypeOf(v_sub) len += @inline(format_inner, TSub, v_sub, str + len, opts) i += 1 if i == @len(T) break else { *@as(^[u8; 2], @bitcast(str + len)) = *@bitcast(", \0") len += 2 } }; *@as(^[u8; 1], @bitcast(str + len)) = *@bitcast(")\0") len += 1 return len }, _ => panic("unsupported format type\0"), } } /* TODO: * Custom formatters using struct methods (T._fmt(self, str): uint), * Format struct names "Name.(x, y, z)" * Format struct fields "Name.{a: x, b: y, c: z}" * Optionally tabulate * Add more FormatOption fields * Support scientific notation for floating point * Support format string (impossible right now) * Support nullables (impossible right now) * Support pointers */ FormatOptions := struct { decimal_digits: uint = 1 << 32, radix: uint = 10, } /* SAFETY: * Assumes the buffer is wide enough for the formatted text and a null char */ format := fn($T: type, v: T, str: ^u8): ^u8 return @inline(format_args, T, v, str, .{}) format_args := fn($T: type, v: T, str: ^u8, opts: FormatOptions): ^u8 { @inline(string.clear, str) _ = @inline(format_inner, T, v, str, opts) return str }