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:...@@ -8493,7 +8493,7 @@ fn numberLiteral(gz: *GenZir, ri: ResultInfo, node: Ast.Node.Index, source_node:
8493 },8493 },
8494 .float => {8494 .float => {
8495 const unsigned_float_number = std.fmt.parseFloat(f128, bytes) catch |err| switch (err) {8495 const unsigned_float_number = std.fmt.parseFloat(f128, bytes) catch |err| switch (err) {
8496 error.InvalidCharacter => unreachable, // validated by tokenizer8496 error.InvalidCharacter => unreachable, // validated by `parseNumberLiteral`
8497 };8497 };
8498 const float_number = switch (sign) {8498 const float_number = switch (sign) {
8499 .negative => -unsigned_float_number,8499 .negative => -unsigned_float_number,
lib/std/zig/number_literal.zig+41-14
...@@ -1,7 +1,5 @@...@@ -1,7 +1,5 @@
1const std = @import("../std.zig");1const std = @import("../std.zig");
2const assert = std.debug.assert;2const assert = std.debug.assert;
3const utf8Decode = std.unicode.utf8Decode;
4const utf8Encode = std.unicode.utf8Encode;
53
6pub const ParseError = error{4pub const ParseError = error{
7 OutOfMemory,5 OutOfMemory,
...@@ -46,7 +44,7 @@ pub const Error = union(enum) {...@@ -46,7 +44,7 @@ pub const Error = union(enum) {
46 duplicate_exponent: usize,44 duplicate_exponent: usize,
47 /// Exponent comes directly after '_' digit separator.45 /// Exponent comes directly after '_' digit separator.
48 exponent_after_underscore: usize,46 exponent_after_underscore: usize,
49 /// Special character (+-.) comes directly after exponent.47 /// Special character (+-.) comes directly after underscore.
50 special_after_underscore: usize,48 special_after_underscore: usize,
51 /// Number ends in special character (+-.)49 /// Number ends in special character (+-.)
52 trailing_special: usize,50 trailing_special: usize,
...@@ -56,13 +54,15 @@ pub const Error = union(enum) {...@@ -56,13 +54,15 @@ pub const Error = union(enum) {
56 invalid_character: usize,54 invalid_character: usize,
57 /// [+-] not immediately after [pPeE]55 /// [+-] not immediately after [pPeE]
58 invalid_exponent_sign: usize,56 invalid_exponent_sign: usize,
59 /// Period comes directly after exponent.57 /// Period comes after exponent.
60 period_after_exponent: usize,58 period_after_exponent: usize,
61};59};
6260
63/// Parse Zig number literal accepted by fmt.parseInt, fmt.parseFloat and big_int.setString.61/// 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.
65pub fn parseNumberLiteral(bytes: []const u8) Result {63pub fn parseNumberLiteral(bytes: []const u8) Result {
64 // This is enforced by the tokenizer.
65 assert(bytes.len > 0 and std.ascii.isDigit(bytes[0]));
66 var i: usize = 0;66 var i: usize = 0;
67 var base: u8 = 10;67 var base: u8 = 10;
68 if (bytes.len >= 2 and bytes[0] == '0') switch (bytes[1]) {68 if (bytes.len >= 2 and bytes[0] == '0') switch (bytes[1]) {
...@@ -121,15 +121,7 @@ pub fn parseNumberLiteral(bytes: []const u8) Result {...@@ -121,15 +121,7 @@ pub fn parseNumberLiteral(bytes: []const u8) Result {
121 continue;121 continue;
122 },122 },
123 '.' => {123 '.' => {
124 if (exponent) {124 if (exponent) return .{ .failure = .{ .period_after_exponent = i } };
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 }
133 float = true;125 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 = 2 } };
135 if (period) return .{ .failure = .duplicate_period };127 if (period) return .{ .failure = .duplicate_period };
...@@ -177,3 +169,38 @@ pub fn parseNumberLiteral(bytes: []const u8) Result {...@@ -177,3 +169,38 @@ pub fn parseNumberLiteral(bytes: []const u8) Result {
177 if (overflow) return .{ .big_int = @as(Base, @enumFromInt(base)) };169 if (overflow) return .{ .big_int = @as(Base, @enumFromInt(base)) };
178 return .{ .int = x };170 return .{ .int = x };
179}171}
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 {...@@ -10,6 +10,9 @@ comptime {
10comptime {10comptime {
11 _ = 12E.0;11 _ = 12E.0;
12}12}
13comptime {
14 _ = 12E1.0;
15}
13comptime {16comptime {
14 _ = 0xp0;17 _ = 0xp0;
15}18}
...@@ -23,5 +26,6 @@ comptime {...@@ -23,5 +26,6 @@ comptime {
23// :5:11: error: unexpected period after exponent26// :5:11: error: unexpected period after exponent
24// :8:12: error: unexpected period after exponent27// :8:12: error: unexpected period after exponent
25// :11:12: error: unexpected period after exponent28// :11:12: error: unexpected period after exponent
26// :14:9: error: expected a digit after base prefix29// :14:13: error: unexpected period after exponent
27// :17:9: error: expected a digit after base prefix30// :17:9: error: expected a digit after base prefix
31// :20:9: error: expected a digit after base prefix