authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-07-14 14:59:22-07:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-07-14 15:04:40-07:00
loga52363468abaf07441dcc3002e69090a9d470ffd
treea4ef847983c3593eb52e972c6a03c220ab0b668a
parentaddc3c3b8cfb03be7ddee89949eccb22af793887

Ensure that parseNumberLiteral fails for all inputs that parseFloat fails on

AstGen relies on this behavior, since it marks InvalidCharacter from parseFloat as unreachable after parseNumberLiteral returns float. Before this commit, there was a mismatch, since parseNumberLiteral was not failing on literals with a period in the exponent. Fixes #36161

3 files changed, 47 insertions(+), 16 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+41-14
......@@ -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,15 +121,7 @@ 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;
134126 if (base != 10 and base != 16) return .{ .failure = .{ .invalid_float_base = 2 } };
135127 if (period) return .{ .failure = .duplicate_period };
......@@ -177,3 +169,38 @@ 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_digit = .{ .i = 3, .base = .octal } } }, parseNumberLiteral("0o3e1"));
181}
182
183/// Returns an error if `parseNumberLiteral` returns `.float` but `parseFloat` fails.
184/// AstGen relies on `parseFloat` being unable to fail after calling `parseNumberLiteral`.
185fn checkFloat(bytes: []const u8) !void {
186 // Number literals must start with a digit
187 if (bytes.len == 0 or !std.ascii.isDigit(bytes[0])) return;
188
189 switch (parseNumberLiteral(bytes)) {
190 .float => {
191 _ = try std.fmt.parseFloat(f128, bytes);
192 },
193 else => {},
194 }
195}
196
197test "parseNumberLiteral float validation" {
198 const Context = struct {
199 fn testOne(_: @This(), smith: *std.testing.Smith) anyerror!void {
200 var buf: [256]u8 = undefined;
201 const bytes = buf[0..smith.slice(&buf)];
202 try checkFloat(bytes);
203 }
204 };
205 return std.testing.fuzz(Context{}, Context.testOne, .{});
206}
test/cases/compile_errors/invalid_number_literals.zig+5-1
......@@ -10,6 +10,9 @@ comptime {
1010comptime {
1111 _ = 12E.0;
1212}
13comptime {
14 _ = 12E1.0;
15}
1316comptime {
1417 _ = 0xp0;
1518}
......@@ -23,5 +26,6 @@ comptime {
2326// :5:11: error: unexpected period after exponent
2427// :8:12: error: unexpected period after exponent
2528// :11:12: error: unexpected period after exponent
26// :14:9: error: expected a digit after base prefix
29// :14:13: error: unexpected period after exponent
2730// :17:9: error: expected a digit after base prefix
31// :20:9: error: expected a digit after base prefix