also looking at this
fix(parser): accept prefixed and suffixed numeric literals in match patterns
#927
2 files
+103
-6
| @@ -5003,6 +5003,48 @@ pub const Parser = struct { | ||
| 5003 | 5003 | return ast.Stmt{ .MatchStmt = stmt }; |
| 5004 | 5004 | } |
| 5005 | 5005 | |
| 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 | ||
| 5006 | 5048 | /// Parse a pattern for match statements |
| 5007 | 5049 | fn parsePattern(self: *Parser) !*ast.Pattern { |
| 5008 | 5050 | const pattern = try self.allocator.create(ast.Pattern); |
| @@ -5011,8 +5053,7 @@ pub const Parser = struct { | ||
| 5011 | 5053 | // Negative integer pattern: -N |
| 5012 | 5054 | if (self.match(&.{.Minus})) { |
| 5013 | 5055 | 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()); | |
| 5016 | 5057 | pattern.* = ast.Pattern{ .IntLiteral = -pos_value }; |
| 5017 | 5058 | return pattern; |
| 5018 | 5059 | } |
| @@ -5023,7 +5064,7 @@ pub const Parser = struct { | ||
| 5023 | 5064 | // Integer literal pattern (or range pattern) |
| 5024 | 5065 | if (self.match(&.{.Integer})) { |
| 5025 | 5066 | const token = self.previous(); |
| 5026 | const start_value = try std.fmt.parseInt(i64, token.lexeme, 10); | |
| 5067 | const start_value = try self.patternIntValue(token); | |
| 5027 | 5068 | |
| 5028 | 5069 | // Check for range pattern: N..M (exclusive) or N..=M (inclusive). |
| 5029 | 5070 | // The lexer emits DotDotEqual as a single token, so we must |
| @@ -5033,7 +5074,7 @@ pub const Parser = struct { | ||
| 5033 | 5074 | if (self.match(&.{ .DotDot, .DotDotEqual })) { |
| 5034 | 5075 | const inclusive = self.previous().type == .DotDotEqual; |
| 5035 | 5076 | 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); | |
| 5037 | 5078 | |
| 5038 | 5079 | if (start_value > end_value) { |
| 5039 | 5080 | std.debug.print( |
| @@ -5060,8 +5101,7 @@ pub const Parser = struct { | ||
| 5060 | 5101 | // Float literal pattern — reject infinity/NaN which would |
| 5061 | 5102 | // silently never match any runtime value. |
| 5062 | 5103 | 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()); | |
| 5065 | 5105 | if (std.math.isInf(value) or std.math.isNan(value)) { |
| 5066 | 5106 | try self.reportError("float pattern overflows to infinity or NaN and will never match"); |
| 5067 | 5107 | return error.UnexpectedToken; |
| @@ -2087,3 +2087,60 @@ test "parser: assert macro preserves a comparison condition" { | ||
| 2087 | 2087 | try testing.expect(assert_stmt.message.?.* == .StringLiteral); |
| 2088 | 2088 | try testing.expectEqualStrings("division by zero", assert_stmt.message.?.StringLiteral.value); |
| 2089 | 2089 | } |
| 2090 | ||
| 2091 | test "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 | ||
| 2130 | test "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 | } | |