Skip to content

Commit 47f267d

Browse files
committed
break off some of std.io into std.fmt, generalize printf
closes #250
1 parent c62db57 commit 47f267d

File tree

6 files changed

+378
-295
lines changed

6 files changed

+378
-295
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ install(FILES "${CMAKE_SOURCE_DIR}/std/elf.zig" DESTINATION "${ZIG_STD_DEST}")
210210
install(FILES "${CMAKE_SOURCE_DIR}/std/empty.zig" DESTINATION "${ZIG_STD_DEST}")
211211
install(FILES "${CMAKE_SOURCE_DIR}/std/endian.zig" DESTINATION "${ZIG_STD_DEST}")
212212
install(FILES "${CMAKE_SOURCE_DIR}/std/errno.zig" DESTINATION "${ZIG_STD_DEST}")
213+
install(FILES "${CMAKE_SOURCE_DIR}/std/fmt.zig" DESTINATION "${ZIG_STD_DEST}")
213214
install(FILES "${CMAKE_SOURCE_DIR}/std/hash_map.zig" DESTINATION "${ZIG_STD_DEST}")
214215
install(FILES "${CMAKE_SOURCE_DIR}/std/index.zig" DESTINATION "${ZIG_STD_DEST}")
215216
install(FILES "${CMAKE_SOURCE_DIR}/std/io.zig" DESTINATION "${ZIG_STD_DEST}")

example/guess_number/main.zig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const std = @import("std");
22
const io = std.io;
3+
const fmt = std.fmt;
34
const Rand = std.rand.Rand;
45
const os = std.os;
56

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

26-
const guess = io.parseUnsigned(u8, line_buf[0...line_len - 1], 10) %% {
27+
const guess = fmt.parseUnsigned(u8, line_buf[0...line_len - 1], 10) %% {
2728
%%io.stdout.printf("Invalid number.\n");
2829
continue;
2930
};

std/fmt.zig

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
const math = @import("math.zig");
2+
const debug = @import("debug.zig");
3+
const assert = debug.assert;
4+
const mem = @import("mem.zig");
5+
6+
const max_f64_digits = 65;
7+
const max_int_digits = 65;
8+
9+
const State = enum { // TODO put inside format function and make sure the name and debug info is correct
10+
Start,
11+
OpenBrace,
12+
CloseBrace,
13+
Integer,
14+
IntegerWidth,
15+
Character,
16+
};
17+
18+
/// Renders fmt string with args, calling output with slices of bytes.
19+
/// Return false from output function and output will not be called again.
20+
/// Returns false if output ever returned false, true otherwise.
21+
pub fn format(context: var, output: fn(@typeOf(context), []const u8)->bool,
22+
comptime fmt: []const u8, args: ...) -> bool
23+
{
24+
comptime var start_index = 0;
25+
comptime var state = State.Start;
26+
comptime var next_arg = 0;
27+
comptime var radix = 0;
28+
comptime var uppercase = false;
29+
comptime var width = 0;
30+
comptime var width_start = 0;
31+
32+
inline for (fmt) |c, i| {
33+
switch (state) {
34+
State.Start => switch (c) {
35+
'{' => {
36+
// TODO if you make this an if statement with && then it breaks
37+
if (start_index < i) {
38+
if (!output(context, fmt[start_index...i]))
39+
return false;
40+
}
41+
state = State.OpenBrace;
42+
},
43+
'}' => {
44+
if (start_index < i) {
45+
if (!output(context, fmt[start_index...i]))
46+
return false;
47+
}
48+
state = State.CloseBrace;
49+
},
50+
else => {},
51+
},
52+
State.OpenBrace => switch (c) {
53+
'{' => {
54+
state = State.Start;
55+
start_index = i;
56+
},
57+
'}' => {
58+
if (!formatValue(args[next_arg], context, output))
59+
return false;
60+
next_arg += 1;
61+
state = State.Start;
62+
start_index = i + 1;
63+
},
64+
'd' => {
65+
radix = 10;
66+
uppercase = false;
67+
width = 0;
68+
state = State.Integer;
69+
},
70+
'x' => {
71+
radix = 16;
72+
uppercase = false;
73+
width = 0;
74+
state = State.Integer;
75+
},
76+
'X' => {
77+
radix = 16;
78+
uppercase = true;
79+
width = 0;
80+
state = State.Integer;
81+
},
82+
'c' => {
83+
state = State.Character;
84+
},
85+
else => @compileError("Unknown format character: " ++ []u8{c}),
86+
},
87+
State.CloseBrace => switch (c) {
88+
'}' => {
89+
state = State.Start;
90+
start_index = i;
91+
},
92+
else => @compileError("Single '}' encountered in format string"),
93+
},
94+
State.Integer => switch (c) {
95+
'}' => {
96+
if (!formatInt(args[next_arg], radix, uppercase, width, context, output))
97+
return false;
98+
next_arg += 1;
99+
state = State.Start;
100+
start_index = i + 1;
101+
},
102+
'0' ... '9' => {
103+
width_start = i;
104+
state = State.IntegerWidth;
105+
},
106+
else => @compileError("Unexpected character in format string: " ++ []u8{c}),
107+
},
108+
State.IntegerWidth => switch (c) {
109+
'}' => {
110+
width = comptime %%parseUnsigned(usize, fmt[width_start...i], 10);
111+
if (!formatInt(args[next_arg], radix, uppercase, width, context, output))
112+
return false;
113+
next_arg += 1;
114+
state = State.Start;
115+
start_index = i + 1;
116+
},
117+
'0' ... '9' => {},
118+
else => @compileError("Unexpected character in format string: " ++ []u8{c}),
119+
},
120+
State.Character => switch (c) {
121+
'}' => {
122+
if (!formatAsciiChar(args[next_arg], context, output))
123+
return false;
124+
next_arg += 1;
125+
state = State.Start;
126+
start_index = i + 1;
127+
},
128+
else => @compileError("Unexpected character in format string: " ++ []u8{c}),
129+
},
130+
}
131+
}
132+
comptime {
133+
if (args.len != next_arg) {
134+
@compileError("Unused arguments");
135+
}
136+
if (state != State.Start) {
137+
@compileError("Incomplete format string: " ++ fmt);
138+
}
139+
}
140+
if (start_index < fmt.len) {
141+
if (!output(context, fmt[start_index...]))
142+
return false;
143+
}
144+
145+
return true;
146+
}
147+
148+
pub fn formatValue(value: var, context: var, output: fn(@typeOf(context), []const u8)->bool) -> bool {
149+
const T = @typeOf(value);
150+
if (@isInteger(T)) {
151+
return formatInt(value, 10, false, 0, context, output);
152+
} else if (@isFloat(T)) {
153+
@compileError("TODO implement formatFloat");
154+
} else if (@canImplicitCast([]const u8, value)) {
155+
const casted_value = ([]const u8)(value);
156+
return output(context, casted_value);
157+
} else if (T == void) {
158+
return output(context, "void");
159+
} else {
160+
@compileError("Unable to format type '" ++ @typeName(T) ++ "'");
161+
}
162+
}
163+
164+
pub fn formatAsciiChar(c: u8, context: var, output: fn(@typeOf(context), []const u8)->bool) -> bool {
165+
return output(context, (&c)[0...1]);
166+
}
167+
168+
pub fn formatInt(value: var, base: u8, uppercase: bool, width: usize,
169+
context: var, output: fn(@typeOf(context), []const u8)->bool) -> bool
170+
{
171+
if (@typeOf(value).is_signed) {
172+
return formatIntSigned(value, base, uppercase, width, context, output);
173+
} else {
174+
return formatIntUnsigned(value, base, uppercase, width, context, output);
175+
}
176+
}
177+
178+
fn formatIntSigned(value: var, base: u8, uppercase: bool, width: usize,
179+
context: var, output: fn(@typeOf(context), []const u8)->bool) -> bool
180+
{
181+
const uint = @intType(false, @typeOf(value).bit_count);
182+
if (value < 0) {
183+
const minus_sign: u8 = '-';
184+
if (!output(context, (&minus_sign)[0...1]))
185+
return false;
186+
const new_value = uint(-(value + 1)) + 1;
187+
const new_width = if (width == 0) 0 else (width - 1);
188+
return formatIntUnsigned(new_value, base, uppercase, new_width, context, output);
189+
} else if (width == 0) {
190+
return formatIntUnsigned(uint(value), base, uppercase, width, context, output);
191+
} else {
192+
const plus_sign: u8 = '+';
193+
if (!output(context, (&plus_sign)[0...1]))
194+
return false;
195+
const new_value = uint(value);
196+
const new_width = if (width == 0) 0 else (width - 1);
197+
return formatIntUnsigned(new_value, base, uppercase, new_width, context, output);
198+
}
199+
}
200+
201+
fn formatIntUnsigned(value: var, base: u8, uppercase: bool, width: usize,
202+
context: var, output: fn(@typeOf(context), []const u8)->bool) -> bool
203+
{
204+
// max_int_digits accounts for the minus sign. when printing an unsigned
205+
// number we don't need to do that.
206+
var buf: [max_int_digits - 1]u8 = undefined;
207+
var a = value;
208+
var index: usize = buf.len;
209+
210+
while (true) {
211+
const digit = a % base;
212+
index -= 1;
213+
buf[index] = digitToChar(u8(digit), uppercase);
214+
a /= base;
215+
if (a == 0)
216+
break;
217+
}
218+
219+
const digits_buf = buf[index...];
220+
const padding = if (width > digits_buf.len) (width - digits_buf.len) else 0;
221+
222+
if (padding > index) {
223+
const zero_byte: u8 = '0';
224+
var leftover_padding = padding - index;
225+
while (true) {
226+
if (!output(context, (&zero_byte)[0...1]))
227+
return false;
228+
leftover_padding -= 1;
229+
if (leftover_padding == 0)
230+
break;
231+
}
232+
mem.set(u8, buf[0...index], '0');
233+
return output(context, buf);
234+
} else {
235+
const padded_buf = buf[index - padding...];
236+
mem.set(u8, padded_buf[0...padding], '0');
237+
return output(context, padded_buf);
238+
}
239+
}
240+
241+
pub fn formatIntBuf(out_buf: []u8, value: var, base: u8, uppercase: bool, width: usize) -> usize {
242+
var context = FormatIntBuf {
243+
.out_buf = out_buf,
244+
.index = 0,
245+
};
246+
_ = formatInt(value, base, uppercase, width, &context, formatIntCallback);
247+
return context.index;
248+
}
249+
const FormatIntBuf = struct {
250+
out_buf: []u8,
251+
index: usize,
252+
};
253+
fn formatIntCallback(context: &FormatIntBuf, bytes: []const u8) -> bool {
254+
mem.copy(u8, context.out_buf[context.index...], bytes);
255+
context.index += bytes.len;
256+
return true;
257+
}
258+
259+
pub fn parseUnsigned(comptime T: type, buf: []const u8, radix: u8) -> %T {
260+
var x: T = 0;
261+
262+
for (buf) |c| {
263+
const digit = %return charToDigit(c, radix);
264+
x = %return math.mulOverflow(T, x, radix);
265+
x = %return math.addOverflow(T, x, digit);
266+
}
267+
268+
return x;
269+
}
270+
271+
error InvalidChar;
272+
fn charToDigit(c: u8, radix: u8) -> %u8 {
273+
const value = switch (c) {
274+
'0' ... '9' => c - '0',
275+
'A' ... 'Z' => c - 'A' + 10,
276+
'a' ... 'z' => c - 'a' + 10,
277+
else => return error.InvalidChar,
278+
};
279+
280+
if (value >= radix)
281+
return error.InvalidChar;
282+
283+
return value;
284+
}
285+
286+
fn digitToChar(digit: u8, uppercase: bool) -> u8 {
287+
return switch (digit) {
288+
0 ... 9 => digit + '0',
289+
10 ... 35 => digit + ((if (uppercase) u8('A') else u8('a')) - 10),
290+
else => @unreachable(),
291+
};
292+
}
293+
294+
fn testBufPrintInt() {
295+
@setFnTest(this);
296+
297+
var buffer: [max_int_digits]u8 = undefined;
298+
const buf = buffer[0...];
299+
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(-12345678), 2, false, 0), "-101111000110000101001110"));
300+
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(-12345678), 10, false, 0), "-12345678"));
301+
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(-12345678), 16, false, 0), "-bc614e"));
302+
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(-12345678), 16, true, 0), "-BC614E"));
303+
304+
assert(mem.eql(u8, bufPrintIntToSlice(buf, u32(12345678), 10, true, 0), "12345678"));
305+
306+
assert(mem.eql(u8, bufPrintIntToSlice(buf, u32(666), 10, false, 6), "000666"));
307+
assert(mem.eql(u8, bufPrintIntToSlice(buf, u32(0x1234), 16, false, 6), "001234"));
308+
assert(mem.eql(u8, bufPrintIntToSlice(buf, u32(0x1234), 16, false, 1), "1234"));
309+
310+
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(42), 10, false, 3), "+42"));
311+
assert(mem.eql(u8, bufPrintIntToSlice(buf, i32(-42), 10, false, 3), "-42"));
312+
}
313+
314+
fn bufPrintIntToSlice(buf: []u8, value: var, base: u8, uppercase: bool, width: usize) -> []u8 {
315+
return buf[0...formatIntBuf(buf, value, base, uppercase, width)];
316+
}
317+
318+
fn testParseU64DigitTooBig() {
319+
@setFnTest(this);
320+
321+
parseUnsigned(u64, "123a", 10) %% |err| {
322+
if (err == error.InvalidChar) return;
323+
@unreachable();
324+
};
325+
@unreachable();
326+
}
327+
328+
fn testParseUnsignedComptime() {
329+
@setFnTest(this);
330+
331+
comptime {
332+
assert(%%parseUnsigned(usize, "2", 10) == 2);
333+
}
334+
}

std/index.zig

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
pub const rand = @import("rand.zig");
2-
pub const io = @import("io.zig");
3-
pub const os = @import("os.zig");
4-
pub const math = @import("math.zig");
51
pub const cstr = @import("cstr.zig");
6-
pub const sort = @import("sort.zig");
7-
pub const net = @import("net.zig");
8-
pub const list = @import("list.zig");
2+
pub const debug = @import("debug.zig");
3+
pub const fmt = @import("fmt.zig");
94
pub const hash_map = @import("hash_map.zig");
5+
pub const io = @import("io.zig");
6+
pub const list = @import("list.zig");
7+
pub const math = @import("math.zig");
108
pub const mem = @import("mem.zig");
11-
pub const debug = @import("debug.zig");
9+
pub const net = @import("net.zig");
10+
pub const os = @import("os.zig");
11+
pub const rand = @import("rand.zig");
12+
pub const sort = @import("sort.zig");
1213
pub const linux = switch(@compileVar("os")) {
1314
Os.linux => @import("linux.zig"),
14-
else => null_import,
15+
else => empty_import,
1516
};
1617
pub const darwin = switch(@compileVar("os")) {
1718
Os.darwin => @import("darwin.zig"),
18-
else => null_import,
19+
else => empty_import,
1920
};
20-
const null_import = @import("empty.zig");
21+
pub const empty_import = @import("empty.zig");

0 commit comments

Comments
 (0)