From a52363468abaf07441dcc3002e69090a9d470ffd Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Tue, 14 Jul 2026 14:59:22 -0700 Subject: [PATCH] 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 --- lib/std/zig/AstGen.zig | 2 +- lib/std/zig/number_literal.zig | 55 ++++++++++++++----- .../invalid_number_literals.zig | 6 +- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index 19057918cea3abe07d8adc61a73062c88d853b14..16e0616ede4ac141162b054a48a36694f12e222f 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -8493,7 +8493,7 @@ fn numberLiteral(gz: *GenZir, ri: ResultInfo, node: Ast.Node.Index, source_node: }, .float => { const unsigned_float_number = std.fmt.parseFloat(f128, bytes) catch |err| switch (err) { - error.InvalidCharacter => unreachable, // validated by tokenizer + error.InvalidCharacter => unreachable, // validated by `parseNumberLiteral` }; const float_number = switch (sign) { .negative => -unsigned_float_number, diff --git a/lib/std/zig/number_literal.zig b/lib/std/zig/number_literal.zig index a4dc33eb91c3f70536431174b9badc5222fe4258..754fefafb586a3674e9e278b094a875f29174979 100644 --- a/lib/std/zig/number_literal.zig +++ b/lib/std/zig/number_literal.zig @@ -1,7 +1,5 @@ const std = @import("../std.zig"); const assert = std.debug.assert; -const utf8Decode = std.unicode.utf8Decode; -const utf8Encode = std.unicode.utf8Encode; pub const ParseError = error{ OutOfMemory, @@ -46,7 +44,7 @@ pub const Error = union(enum) { duplicate_exponent: usize, /// Exponent comes directly after '_' digit separator. exponent_after_underscore: usize, - /// Special character (+-.) comes directly after exponent. + /// Special character (+-.) comes directly after underscore. special_after_underscore: usize, /// Number ends in special character (+-.) trailing_special: usize, @@ -56,13 +54,15 @@ pub const Error = union(enum) { invalid_character: usize, /// [+-] not immediately after [pPeE] invalid_exponent_sign: usize, - /// Period comes directly after exponent. + /// Period comes after exponent. period_after_exponent: usize, }; /// Parse Zig number literal accepted by fmt.parseInt, fmt.parseFloat and big_int.setString. -/// Valid for any input. +/// Valid for any number_literal token bytes. pub fn parseNumberLiteral(bytes: []const u8) Result { + // This is enforced by the tokenizer. + assert(bytes.len > 0 and std.ascii.isDigit(bytes[0])); var i: usize = 0; var base: u8 = 10; if (bytes.len >= 2 and bytes[0] == '0') switch (bytes[1]) { @@ -121,15 +121,7 @@ pub fn parseNumberLiteral(bytes: []const u8) Result { continue; }, '.' => { - if (exponent) { - const digit_index = i - ".e".len; - if (digit_index < bytes.len) { - switch (bytes[digit_index]) { - '0'...'9' => return .{ .failure = .{ .period_after_exponent = i } }, - else => {}, - } - } - } + if (exponent) return .{ .failure = .{ .period_after_exponent = i } }; float = true; if (base != 10 and base != 16) return .{ .failure = .{ .invalid_float_base = 2 } }; if (period) return .{ .failure = .duplicate_period }; @@ -177,3 +169,38 @@ pub fn parseNumberLiteral(bytes: []const u8) Result { if (overflow) return .{ .big_int = @as(Base, @enumFromInt(base)) }; return .{ .int = x }; } + +test parseNumberLiteral { + try std.testing.expectEqual(Result{ .float = .decimal }, parseNumberLiteral("3E2")); + try std.testing.expectEqual(Result{ .int = 0x3E2 }, parseNumberLiteral("0x3E2")); + try std.testing.expectEqual(Result{ .float = .hex }, parseNumberLiteral("0x3p2")); + try std.testing.expectEqual(Result{ .failure = .{ .period_after_exponent = 3 } }, parseNumberLiteral("3E2.5")); + try std.testing.expectEqual(Result{ .failure = .{ .period_after_exponent = 2 } }, parseNumberLiteral("3E.5")); + try std.testing.expectEqual(Result{ .failure = .{ .period_after_exponent = 3 } }, parseNumberLiteral("3E1.")); + try std.testing.expectEqual(Result{ .failure = .{ .invalid_digit = .{ .i = 3, .base = .octal } } }, parseNumberLiteral("0o3e1")); +} + +/// Returns an error if `parseNumberLiteral` returns `.float` but `parseFloat` fails. +/// AstGen relies on `parseFloat` being unable to fail after calling `parseNumberLiteral`. +fn checkFloat(bytes: []const u8) !void { + // Number literals must start with a digit + if (bytes.len == 0 or !std.ascii.isDigit(bytes[0])) return; + + switch (parseNumberLiteral(bytes)) { + .float => { + _ = try std.fmt.parseFloat(f128, bytes); + }, + else => {}, + } +} + +test "parseNumberLiteral float validation" { + const Context = struct { + fn testOne(_: @This(), smith: *std.testing.Smith) anyerror!void { + var buf: [256]u8 = undefined; + const bytes = buf[0..smith.slice(&buf)]; + try checkFloat(bytes); + } + }; + return std.testing.fuzz(Context{}, Context.testOne, .{}); +} diff --git a/test/cases/compile_errors/invalid_number_literals.zig b/test/cases/compile_errors/invalid_number_literals.zig index bd41ba27c9200b83faf619e7c40930287cab7a7e..5732c2bd44d8ee01cbc3a753a7aafbeb8d82e407 100644 --- a/test/cases/compile_errors/invalid_number_literals.zig +++ b/test/cases/compile_errors/invalid_number_literals.zig @@ -10,6 +10,9 @@ comptime { comptime { _ = 12E.0; } +comptime { + _ = 12E1.0; +} comptime { _ = 0xp0; } @@ -23,5 +26,6 @@ comptime { // :5:11: error: unexpected period after exponent // :8:12: error: unexpected period after exponent // :11:12: error: unexpected period after exponent -// :14:9: error: expected a digit after base prefix +// :14:13: error: unexpected period after exponent // :17:9: error: expected a digit after base prefix +// :20:9: error: expected a digit after base prefix -- 2.54.0