ReviewOS

also looking at this

home-lang/home

fix(parser): accept prefixed and suffixed numeric literals in match patterns

#927
Open mkantautas wants to merge fix/match-pattern-int-literals into main
2 files +103 -6

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/parser/src/parser.zigmodified+46-6
Changes to packages/parser/src/parser.zig
@@ -5003,6 +5003,48 @@ pub const Parser = struct {
50035003 return ast.Stmt{ .MatchStmt = stmt };
50045004 }
50055005
5006 /// Digits of a numeric literal token used in a match pattern, without
5007 /// `_` separators or a type suffix (`i<N>`/`u<N>` for integers, `f<N>`
5008 /// for floats). Expression literals accept the same spellings.
5009 fn patternLiteralDigits(self: *Parser, lexeme: []const u8, suffix_starts: []const u8) !std.ArrayList(u8) {
5010 var end = lexeme.len;
5011 for (lexeme, 0..) |c, i| {
5012 if (i > 0 and i + 1 < lexeme.len and std.mem.indexOfScalar(u8, suffix_starts, c) != null and std.ascii.isDigit(lexeme[i + 1])) {
5013 end = i;
5014 break;
5015 }
5016 }
5017
5018 var digits = std.ArrayList(u8).empty;
5019 errdefer digits.deinit(self.allocator);
5020 for (lexeme[0..end]) |c| {
5021 if (c != '_') try digits.append(self.allocator, c);
5022 }
5023 return digits;
5024 }
5025
5026 /// Value of an integer token used in a match pattern. Accepts `0x`/`0b`/`0o`
5027 /// prefixes, `_` separators and a type suffix.
5028 fn patternIntValue(self: *Parser, token: Token) !i64 {
5029 var digits = try self.patternLiteralDigits(token.lexeme, "iu");
5030 defer digits.deinit(self.allocator);
5031 return std.fmt.parseInt(i64, digits.items, 0) catch {
5032 try self.reportError("Invalid integer literal in pattern");
5033 return error.UnexpectedToken;
5034 };
5035 }
5036
5037 /// Value of a float token used in a match pattern. Accepts `_` separators
5038 /// and a type suffix.
5039 fn patternFloatValue(self: *Parser, token: Token) !f64 {
5040 var digits = try self.patternLiteralDigits(token.lexeme, "f");
5041 defer digits.deinit(self.allocator);
5042 return std.fmt.parseFloat(f64, digits.items) catch {
5043 try self.reportError("Invalid float literal in pattern");
5044 return error.UnexpectedToken;
5045 };
5046 }
5047
50065048 /// Parse a pattern for match statements
50075049 fn parsePattern(self: *Parser) !*ast.Pattern {
50085050 const pattern = try self.allocator.create(ast.Pattern);
@@ -5011,8 +5053,7 @@ pub const Parser = struct {
50115053 // Negative integer pattern: -N
50125054 if (self.match(&.{.Minus})) {
50135055 if (self.match(&.{.Integer})) {
5014 const token = self.previous();
5015 const pos_value = try std.fmt.parseInt(i64, token.lexeme, 10);
5056 const pos_value = try self.patternIntValue(self.previous());
50165057 pattern.* = ast.Pattern{ .IntLiteral = -pos_value };
50175058 return pattern;
50185059 }
@@ -5023,7 +5064,7 @@ pub const Parser = struct {
50235064 // Integer literal pattern (or range pattern)
50245065 if (self.match(&.{.Integer})) {
50255066 const token = self.previous();
5026 const start_value = try std.fmt.parseInt(i64, token.lexeme, 10);
5067 const start_value = try self.patternIntValue(token);
50275068
50285069 // Check for range pattern: N..M (exclusive) or N..=M (inclusive).
50295070 // The lexer emits DotDotEqual as a single token, so we must
@@ -5033,7 +5074,7 @@ pub const Parser = struct {
50335074 if (self.match(&.{ .DotDot, .DotDotEqual })) {
50345075 const inclusive = self.previous().type == .DotDotEqual;
50355076 const end_token = try self.expect(.Integer, "Expected end value in range pattern");
5036 const end_value = try std.fmt.parseInt(i64, end_token.lexeme, 10);
5077 const end_value = try self.patternIntValue(end_token);
50375078
50385079 if (start_value > end_value) {
50395080 std.debug.print(
@@ -5060,8 +5101,7 @@ pub const Parser = struct {
50605101 // Float literal pattern — reject infinity/NaN which would
50615102 // silently never match any runtime value.
50625103 if (self.match(&.{.Float})) {
5063 const token = self.previous();
5064 const value = try std.fmt.parseFloat(f64, token.lexeme);
5104 const value = try self.patternFloatValue(self.previous());
50655105 if (std.math.isInf(value) or std.math.isNan(value)) {
50665106 try self.reportError("float pattern overflows to infinity or NaN and will never match");
50675107 return error.UnexpectedToken;
packages/parser/tests/parser_test.zigmodified+57-0
Changes to packages/parser/tests/parser_test.zig
@@ -2087,3 +2087,60 @@ test "parser: assert macro preserves a comparison condition" {
20872087 try testing.expect(assert_stmt.message.?.* == .StringLiteral);
20882088 try testing.expectEqualStrings("division by zero", assert_stmt.message.?.StringLiteral.value);
20892089}
2090
2091test "parser: match statement numeric patterns accept hex, binary, octal and type suffixes" {
2092 const allocator = testing.allocator;
2093 const source =
2094 \\match x {
2095 \\ 0xFF => 1,
2096 \\ 0b101 => 2,
2097 \\ 0o17 => 3,
2098 \\ 7u8 => 4,
2099 \\ -0x10 => 5,
2100 \\ 0x30..=0x39 => 6,
2101 \\ 1.5f32 => 7,
2102 \\ _ => 0,
2103 \\}
2104 ;
2105 var lexer = Lexer.init(allocator, source);
2106 var tokens = try lexer.tokenize();
2107 defer tokens.deinit(allocator);
2108 var parser = try Parser.init(allocator, tokens.items);
2109 defer parser.deinit();
2110 const program = try parser.parse();
2111 defer program.deinit(allocator);
2112
2113 try testing.expectEqual(@as(usize, 0), parser.errors.items.len);
2114 try testing.expectEqual(@as(usize, 1), program.statements.len);
2115 try testing.expect(program.statements[0] == .MatchStmt);
2116
2117 const arms = program.statements[0].MatchStmt.arms;
2118 try testing.expectEqual(@as(usize, 8), arms.len);
2119 try testing.expectEqual(@as(i64, 255), arms[0].pattern.IntLiteral);
2120 try testing.expectEqual(@as(i64, 5), arms[1].pattern.IntLiteral);
2121 try testing.expectEqual(@as(i64, 15), arms[2].pattern.IntLiteral);
2122 try testing.expectEqual(@as(i64, 7), arms[3].pattern.IntLiteral);
2123 try testing.expectEqual(@as(i64, -16), arms[4].pattern.IntLiteral);
2124 try testing.expectEqual(@as(i128, 0x30), arms[5].pattern.Range.start.IntegerLiteral.value);
2125 try testing.expectEqual(@as(i128, 0x39), arms[5].pattern.Range.end.IntegerLiteral.value);
2126 try testing.expectEqual(@as(f64, 1.5), arms[6].pattern.FloatLiteral);
2127 try testing.expect(arms[7].pattern.* == .Wildcard);
2128}
2129
2130test "parser: match statement reports numeric patterns that do not parse" {
2131 const allocator = testing.allocator;
2132 for ([_][]const u8{
2133 "match x {\n 0xFFFFFFFFFFFFFFFF => 1,\n _ => 0,\n}",
2134 "match x {\n 1.0e+ => 1,\n _ => 0,\n}",
2135 }) |source| {
2136 var lexer = Lexer.init(allocator, source);
2137 var tokens = try lexer.tokenize();
2138 defer tokens.deinit(allocator);
2139 var parser = try Parser.init(allocator, tokens.items);
2140 defer parser.deinit();
2141 const program = try parser.parse();
2142 defer program.deinit(allocator);
2143
2144 try testing.expect(parser.errors.items.len > 0);
2145 }
2146}