authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-07-16 01:28:37+02:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-07-16 01:28:37+02:00
log5d08e47160ade85f0b47f925f9aa32b66827e82e
tree29a20d11e9ef9cdaf50116112c833a3b3a729d20
parent20befa4e64f1654fa3c69a0e7b0b194193c2ab27
parent569381f60b86293c60089780006d4ff69f527e70

Merge pull request 'Ensure that parseNumberLiteral fails for all inputs that parseFloat fails on' (#36170) from squeek502/zig:number-literal-parsing into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/36170

3 files changed, 53 insertions(+), 17 deletions(-)

lib/std/zig/AstGen.zig+1-1
......@@ -8493,7 +8493,7 @@ fn numberLiteral(gz: *GenZir, ri: ResultInfo, node: Ast.Node.Index, source_node:
84938493 },
84948494 .float => {
84958495 const unsigned_float_number = std.fmt.parseFloat(f128, bytes) catch |err| switch (err) {
8496 error.InvalidCharacter => unreachable, // validated by tokenizer
8496 error.InvalidCharacter => unreachable, // validated by `parseNumberLiteral`
84978497 };
84988498 const float_number = switch (sign) {
84998499 .negative => -unsigned_float_number,
lib/std/zig/number_literal.zig+43-15
......@@ -1,7 +1,5 @@
11const std = @import("../std.zig");
22const assert = std.debug.assert;
3const utf8Decode = std.unicode.utf8Decode;
4const utf8Encode = std.unicode.utf8Encode;
53
64pub const ParseError = error{
75 OutOfMemory,
......@@ -46,7 +44,7 @@ pub const Error = union(enum) {
4644 duplicate_exponent: usize,
4745 /// Exponent comes directly after '_' digit separator.
4846 exponent_after_underscore: usize,
49 /// Special character (+-.) comes directly after exponent.
47 /// Special character (+-.) comes directly after underscore.
5048 special_after_underscore: usize,
5149 /// Number ends in special character (+-.)
5250 trailing_special: usize,
......@@ -56,13 +54,15 @@ pub const Error = union(enum) {
5654 invalid_character: usize,
5755 /// [+-] not immediately after [pPeE]
5856 invalid_exponent_sign: usize,
59 /// Period comes directly after exponent.
57 /// Period comes after exponent.
6058 period_after_exponent: usize,
6159};
6260
6361/// Parse Zig number literal accepted by fmt.parseInt, fmt.parseFloat and big_int.setString.
64/// Valid for any input.
62/// Valid for any number_literal token bytes.
6563pub fn parseNumberLiteral(bytes: []const u8) Result {
64 // This is enforced by the tokenizer.
65 assert(bytes.len > 0 and std.ascii.isDigit(bytes[0]));
6666 var i: usize = 0;
6767 var base: u8 = 10;
6868 if (bytes.len >= 2 and bytes[0] == '0') switch (bytes[1]) {
......@@ -121,17 +121,9 @@ pub fn parseNumberLiteral(bytes: []const u8) Result {
121121 continue;
122122 },
123123 '.' => {
124 if (exponent) {
125 const digit_index = i - ".e".len;
126 if (digit_index < bytes.len) {
127 switch (bytes[digit_index]) {
128 '0'...'9' => return .{ .failure = .{ .period_after_exponent = i } },
129 else => {},
130 }
131 }
132 }
124 if (exponent) return .{ .failure = .{ .period_after_exponent = i } };
133125 float = true;
134 if (base != 10 and base != 16) return .{ .failure = .{ .invalid_float_base = 2 } };
126 if (base != 10 and base != 16) return .{ .failure = .{ .invalid_float_base = 1 } };
135127 if (period) return .{ .failure = .duplicate_period };
136128 period = true;
137129 if (underscore) return .{ .failure = .{ .special_after_underscore = i } };
......@@ -177,3 +169,39 @@ pub fn parseNumberLiteral(bytes: []const u8) Result {
177169 if (overflow) return .{ .big_int = @as(Base, @enumFromInt(base)) };
178170 return .{ .int = x };
179171}
172
173test parseNumberLiteral {
174 try std.testing.expectEqual(Result{ .float = .decimal }, parseNumberLiteral("3E2"));
175 try std.testing.expectEqual(Result{ .int = 0x3E2 }, parseNumberLiteral("0x3E2"));
176 try std.testing.expectEqual(Result{ .float = .hex }, parseNumberLiteral("0x3p2"));
177 try std.testing.expectEqual(Result{ .failure = .{ .period_after_exponent = 3 } }, parseNumberLiteral("3E2.5"));
178 try std.testing.expectEqual(Result{ .failure = .{ .period_after_exponent = 2 } }, parseNumberLiteral("3E.5"));
179 try std.testing.expectEqual(Result{ .failure = .{ .period_after_exponent = 3 } }, parseNumberLiteral("3E1."));
180 try std.testing.expectEqual(Result{ .failure = .{ .invalid_float_base = 1 } }, parseNumberLiteral("0o3.1"));
181 try std.testing.expectEqual(Result{ .failure = .{ .invalid_digit = .{ .i = 3, .base = .octal } } }, parseNumberLiteral("0o3e1"));
182}
183
184/// Returns an error if `parseNumberLiteral` returns `.float` but `parseFloat` fails.
185/// AstGen relies on `parseFloat` being unable to fail after calling `parseNumberLiteral`.
186fn checkFloat(bytes: []const u8) !void {
187 // Number literals must start with a digit
188 if (bytes.len == 0 or !std.ascii.isDigit(bytes[0])) return;
189
190 switch (parseNumberLiteral(bytes)) {
191 .float => {
192 _ = try std.fmt.parseFloat(f128, bytes);
193 },
194 else => {},
195 }
196}
197
198test "parseNumberLiteral float validation" {
199 const Context = struct {
200 fn testOne(_: @This(), smith: *std.testing.Smith) anyerror!void {
201 var buf: [256]u8 = undefined;
202 const bytes = buf[0..smith.slice(&buf)];
203 try checkFloat(bytes);
204 }
205 };
206 return std.testing.fuzz(Context{}, Context.testOne, .{});
207}
test/cases/compile_errors/invalid_number_literals.zig+9-1
......@@ -10,12 +10,18 @@ comptime {
1010comptime {
1111 _ = 12E.0;
1212}
13comptime {
14 _ = 12E1.0;
15}
1316comptime {
1417 _ = 0xp0;
1518}
1619comptime {
1720 _ = 0xP0;
1821}
22comptime {
23 _ = 0o1.0;
24}
1925
2026// error
2127//
......@@ -23,5 +29,7 @@ comptime {
2329// :5:11: error: unexpected period after exponent
2430// :8:12: error: unexpected period after exponent
2531// :11:12: error: unexpected period after exponent
26// :14:9: error: expected a digit after base prefix
32// :14:13: error: unexpected period after exponent
2733// :17:9: error: expected a digit after base prefix
34// :20:9: error: expected a digit after base prefix
35// :23:10: error: invalid base for float literal