Skip to content

Commit

Permalink
break off some of std.io into std.fmt, generalize printf
Browse files Browse the repository at this point in the history
closes #250
  • Loading branch information
andrewrk committed Mar 10, 2017
1 parent c62db57 commit 47f267d
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 295 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ install(FILES "${CMAKE_SOURCE_DIR}/std/elf.zig" DESTINATION "${ZIG_STD_DEST}")
install(FILES "${CMAKE_SOURCE_DIR}/std/empty.zig" DESTINATION "${ZIG_STD_DEST}")
install(FILES "${CMAKE_SOURCE_DIR}/std/endian.zig" DESTINATION "${ZIG_STD_DEST}")
install(FILES "${CMAKE_SOURCE_DIR}/std/errno.zig" DESTINATION "${ZIG_STD_DEST}")
install(FILES "${CMAKE_SOURCE_DIR}/std/fmt.zig" DESTINATION "${ZIG_STD_DEST}")
install(FILES "${CMAKE_SOURCE_DIR}/std/hash_map.zig" DESTINATION "${ZIG_STD_DEST}")
install(FILES "${CMAKE_SOURCE_DIR}/std/index.zig" DESTINATION "${ZIG_STD_DEST}")
install(FILES "${CMAKE_SOURCE_DIR}/std/io.zig" DESTINATION "${ZIG_STD_DEST}")
Expand Down
3 changes: 2 additions & 1 deletion example/guess_number/main.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const std = @import("std");
const io = std.io;
const fmt = std.fmt;
const Rand = std.rand.Rand;
const os = std.os;

Expand All @@ -23,7 +24,7 @@ pub fn main(args: [][]u8) -> %void {
return err;
};

const guess = io.parseUnsigned(u8, line_buf[0...line_len - 1], 10) %% {
const guess = fmt.parseUnsigned(u8, line_buf[0...line_len - 1], 10) %% {
%%io.stdout.printf("Invalid number.\n");
continue;
};
Expand Down
334 changes: 334 additions & 0 deletions std/fmt.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
const math = @import("math.zig");
const debug = @import("debug.zig");
const assert = debug.assert;
const mem = @import("mem.zig");

const max_f64_digits = 65;
const max_int_digits = 65;

const State = enum { // TODO put inside format function and make sure the name and debug info is correct
Start,
OpenBrace,
CloseBrace,
Integer,
IntegerWidth,
Character,
};

/// Renders fmt string with args, calling output with slices of bytes.
/// Return false from output function and output will not be called again.
/// Returns false if output ever returned false, true otherwise.
pub fn format(context: var, output: fn(@typeOf(context), []const u8)->bool,
comptime fmt: []const u8, args: ...) -> bool
{
comptime var start_index = 0;
comptime var state = State.Start;
comptime var next_arg = 0;
comptime var radix = 0;
comptime var uppercase = false;
comptime var width = 0;
comptime var width_start = 0;

inline for (fmt) |c, i| {
switch (state) {
State.Start => switch (c) {
'{' => {
// TODO if you make this an if statement with && then it breaks
if (start_index < i) {
if (!output(context, fmt[start_index...i]))
return false;
}
state = State.OpenBrace;
},
'}' => {
if (start_index < i) {
if (!output(context, fmt[start_index...i]))
return false;
}
state = State.CloseBrace;
},
else => {},
},
State.OpenBrace => switch (c) {
'{' => {
state = State.Start;
start_index = i;
},
'}' => {
if (!formatValue(args[next_arg], context, output))
return false;
next_arg += 1;
state = State.Start;
start_index = i + 1;
},
'd' => {
radix = 10;
uppercase = false;
width = 0;
state = State.Integer;
},
'x' => {
radix = 16;
uppercase = false;
width = 0;
state = State.Integer;
},
'X' => {
radix = 16;
uppercase = true;
width = 0;
state = State.Integer;
},
'c' => {
state = State.Character;
},
else => @compileError("Unknown format character: " ++ []u8{c}),
},
State.CloseBrace => switch (c) {
'}' => {
state = State.Start;
start_index = i;
},
else => @compileError("Single '}' encountered in format string"),
},
State.Integer => switch (c) {
'}' => {
if (!formatInt(args[next_arg], radix, uppercase, width, context, output))
return false;
next_arg += 1;
state = State.Start;
start_index = i + 1;
},
'0' ... '9' => {
width_start = i;
state = State.IntegerWidth;
},
else => @compileError("Unexpected character in format string: " ++ []u8{c}),
},
State.IntegerWidth => switch (c) {
'}' => {
width = comptime %%parseUnsigned(usize, fmt[width_start...i], 10);
if (!formatInt(args[next_arg], radix, uppercase, width, context, output))
return false;
next_arg += 1;
state = State.Start;
start_index = i + 1;
},
'0' ... '9' => {},
else => @compileError("Unexpected character in format string: " ++ []u8{c}),
},
State.Character => switch (c) {
'}' => {
if (!formatAsciiChar(args[next_arg], context, output))
return false;
next_arg += 1;
state = State.Start;
start_index = i + 1;
},
else => @compileError("Unexpected character in format string: " ++ []u8{c}),
},
}
}
comptime {
if (args.len != next_arg) {
@compileError("Unused arguments");
}
if (state != State.Start) {
@compileError("Incomplete format string: " ++ fmt);
}
}
if (start_index < fmt.len) {
if (!output(context, fmt[start_index...]))
return false;
}

return true;
}

pub fn formatValue(value: var, context: var, output: fn(@typeOf(context), []const u8)->bool) -> bool {
const T = @typeOf(value);
if (@isInteger(T)) {
return formatInt(value, 10, false, 0, context, output);
} else if (@isFloat(T)) {
@compileError("TODO implement formatFloat");
} else if (@canImplicitCast([]const u8, value)) {
const casted_value = ([]const u8)(value);
return output(context, casted_value);
} else if (T == void) {
return output(context, "void");
} else {
@compileError("Unable to format type '" ++ @typeName(T) ++ "'");
}
}

pub fn formatAsciiChar(c: u8, context: var, output: fn(@typeOf(context), []const u8)->bool) -> bool {
return output(context, (&c)[0...1]);
}

pub fn formatInt(value: var, base: u8, uppercase: bool, width: usize,
context: var, output: fn(@typeOf(context), []const u8)->bool) -> bool
{
if (@typeOf(value).is_signed) {
return formatIntSigned(value, base, uppercase, width, context, output);
} else {
return formatIntUnsigned(value, base, uppercase, width, context, output);
}
}

fn formatIntSigned(value: var, base: u8, uppercase: bool, width: usize,
context: var, output: fn(@typeOf(context), []const u8)->bool) -> bool
{
const uint = @intType(false, @typeOf(value).bit_count);
if (value < 0) {
const minus_sign: u8 = '-';
if (!output(context, (&minus_sign)[0...1]))
return false;
const new_value = uint(-(value + 1)) + 1;
const new_width = if (width == 0) 0 else (width - 1);
return formatIntUnsigned(new_value, base, uppercase, new_width, context, output);
} else if (width == 0) {
return formatIntUnsigned(uint(value), base, uppercase, width, context, output);
} else {
const plus_sign: u8 = '+';
if (!output(context, (&plus_sign)[0...1]))
return false;
const new_value = uint(value);
const new_width = if (width == 0) 0 else (width - 1);
return formatIntUnsigned(new_value, base, uppercase, new_width, context, output);
}
}

fn formatIntUnsigned(value: var, base: u8, uppercase: bool, width: usize,
context: var, output: fn(@typeOf(context), []const u8)->bool) -> bool
{
// max_int_digits accounts for the minus sign. when printing an unsigned
// number we don't need to do that.
var buf: [max_int_digits - 1]u8 = undefined;
var a = value;
var index: usize = buf.len;

while (true) {
const digit = a % base;
index -= 1;
buf[index] = digitToChar(u8(digit), uppercase);
a /= base;
if (a == 0)
break;
}

const digits_buf = buf[index...];
const padding = if (width > digits_buf.len) (width - digits_buf.len) else 0;

if (padding > index) {
const zero_byte: u8 = '0';
var leftover_padding = padding - index;
while (true) {
if (!output(context, (&zero_byte)[0...1]))
return false;
leftover_padding -= 1;
if (leftover_padding == 0)
break;
}
mem.set(u8, buf[0...index], '0');
return output(context, buf);
} else {
const padded_buf = buf[index - padding...];
mem.set(u8, padded_buf[0...padding], '0');
return output(context, padded_buf);
}
}

pub fn formatIntBuf(out_buf: []u8, value: var, base: u8, uppercase: bool, width: usize) -> usize {
var context = FormatIntBuf {
.out_buf = out_buf,
.index = 0,
};
_ = formatInt(value, base, uppercase, width, &context, formatIntCallback);
return context.index;
}
const FormatIntBuf = struct {
out_buf: []u8,
index: usize,
};
fn formatIntCallback(context: &FormatIntBuf, bytes: []const u8) -> bool {
mem.copy(u8, context.out_buf[context.index...], bytes);
context.index += bytes.len;
return true;
}

pub fn parseUnsigned(comptime T: type, buf: []const u8, radix: u8) -> %T {
var x: T = 0;

for (buf) |c| {
const digit = %return charToDigit(c, radix);
x = %return math.mulOverflow(T, x, radix);
x = %return math.addOverflow(T, x, digit);
}

return x;
}

error InvalidChar;
fn charToDigit(c: u8, radix: u8) -> %u8 {
const value = switch (c) {
'0' ... '9' => c - '0',
'A' ... 'Z' => c - 'A' + 10,
'a' ... 'z' => c - 'a' + 10,
else => return error.InvalidChar,
};

if (value >= radix)
return error.InvalidChar;

return value;
}

fn digitToChar(digit: u8, uppercase: bool) -> u8 {
return switch (digit) {
0 ... 9 => digit + '0',
10 ... 35 => digit + ((if (uppercase) u8('A') else u8('a')) - 10),
else => @unreachable(),
};
}

fn testBufPrintInt() {
@setFnTest(this);

var buffer: [max_int_digits]u8 = undefined;
const buf = buffer[0...];
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(-12345678), 2, false, 0), "-101111000110000101001110"));
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(-12345678), 10, false, 0), "-12345678"));
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(-12345678), 16, false, 0), "-bc614e"));
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(-12345678), 16, true, 0), "-BC614E"));

assert(mem.eql(u8, bufPrintIntToSlice(buf, u32(12345678), 10, true, 0), "12345678"));

assert(mem.eql(u8, bufPrintIntToSlice(buf, u32(666), 10, false, 6), "000666"));
assert(mem.eql(u8, bufPrintIntToSlice(buf, u32(0x1234), 16, false, 6), "001234"));
assert(mem.eql(u8, bufPrintIntToSlice(buf, u32(0x1234), 16, false, 1), "1234"));

assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(42), 10, false, 3), "+42"));
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(-42), 10, false, 3), "-42"));
}

fn bufPrintIntToSlice(buf: []u8, value: var, base: u8, uppercase: bool, width: usize) -> []u8 {
return buf[0...formatIntBuf(buf, value, base, uppercase, width)];
}

fn testParseU64DigitTooBig() {
@setFnTest(this);

parseUnsigned(u64, "123a", 10) %% |err| {
if (err == error.InvalidChar) return;
@unreachable();
};
@unreachable();
}

fn testParseUnsignedComptime() {
@setFnTest(this);

comptime {
assert(%%parseUnsigned(usize, "2", 10) == 2);
}
}
23 changes: 12 additions & 11 deletions std/index.zig
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
pub const rand = @import("rand.zig");
pub const io = @import("io.zig");
pub const os = @import("os.zig");
pub const math = @import("math.zig");
pub const cstr = @import("cstr.zig");
pub const sort = @import("sort.zig");
pub const net = @import("net.zig");
pub const list = @import("list.zig");
pub const debug = @import("debug.zig");
pub const fmt = @import("fmt.zig");
pub const hash_map = @import("hash_map.zig");
pub const io = @import("io.zig");
pub const list = @import("list.zig");
pub const math = @import("math.zig");
pub const mem = @import("mem.zig");
pub const debug = @import("debug.zig");
pub const net = @import("net.zig");
pub const os = @import("os.zig");
pub const rand = @import("rand.zig");
pub const sort = @import("sort.zig");
pub const linux = switch(@compileVar("os")) {
Os.linux => @import("linux.zig"),
else => null_import,
else => empty_import,
};
pub const darwin = switch(@compileVar("os")) {
Os.darwin => @import("darwin.zig"),
else => null_import,
else => empty_import,
};
const null_import = @import("empty.zig");
pub const empty_import = @import("empty.zig");
Loading

0 comments on commit 47f267d

Please sign in to comment.