From a52363468abaf07441dcc3002e69090a9d470ffd Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Tue, 14 Jul 2026 14:59:22 -0700 Subject: [PATCH 1/2] 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 From 569381f60b86293c60089780006d4ff69f527e70 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Tue, 14 Jul 2026 15:00:39 -0700 Subject: [PATCH 2/2] parseNumberLiteral: Make "invalid base for float literal" errors point to the base --- lib/std/zig/number_literal.zig | 3 ++- test/cases/compile_errors/invalid_number_literals.zig | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/std/zig/number_literal.zig b/lib/std/zig/number_literal.zig index 754fefafb586a3674e9e278b094a875f29174979..45be08e0de9b63df8aff4a07661571121ac02f2f 100644 --- a/lib/std/zig/number_literal.zig +++ b/lib/std/zig/number_literal.zig @@ -123,7 +123,7 @@ pub fn parseNumberLiteral(bytes: []const u8) Result { '.' => { if (exponent) return .{ .failure = .{ .period_after_exponent = i } }; float = true; - if (base != 10 and base != 16) return .{ .failure = .{ .invalid_float_base = 2 } }; + if (base != 10 and base != 16) return .{ .failure = .{ .invalid_float_base = 1 } }; if (period) return .{ .failure = .duplicate_period }; period = true; if (underscore) return .{ .failure = .{ .special_after_underscore = i } }; @@ -177,6 +177,7 @@ test parseNumberLiteral { 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_float_base = 1 } }, parseNumberLiteral("0o3.1")); try std.testing.expectEqual(Result{ .failure = .{ .invalid_digit = .{ .i = 3, .base = .octal } } }, parseNumberLiteral("0o3e1")); } diff --git a/test/cases/compile_errors/invalid_number_literals.zig b/test/cases/compile_errors/invalid_number_literals.zig index 5732c2bd44d8ee01cbc3a753a7aafbeb8d82e407..bd69a046e853fe2bee464901efb1bedb884cb314 100644 --- a/test/cases/compile_errors/invalid_number_literals.zig +++ b/test/cases/compile_errors/invalid_number_literals.zig @@ -19,6 +19,9 @@ comptime { comptime { _ = 0xP0; } +comptime { + _ = 0o1.0; +} // error // @@ -29,3 +32,4 @@ comptime { // :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 +// :23:10: error: invalid base for float literal -- 2.54.0