From 02aaa098f4151e5cc1fa9d9dd79757e0ae3821c1 Mon Sep 17 00:00:00 2001 From: Darek Date: Sun, 18 Oct 2026 04:10:09 +0200 Subject: [PATCH] perf(parser): append sorted intervals without scanning the set The first query in a process deserializes the lexer's ATN, which builds large Unicode character sets one interval at a time. IntervalSet::add walked the whole list for each one, which was about half of the first query's compile time. An interval that goes into an empty set, or starts after the last one without touching it, is now appended directly; the result is the same. This is a local patch on the vendored ANTLR 4.13.1. Co-Authored-By: Claude Opus 5.5 --- test/common/CMakeLists.txt | 1 + test/common/antlr_interval_set_test.cpp | 94 +++++++++++++++++++ .../antlr4_runtime/src/misc/IntervalSet.cpp | 9 ++ 3 files changed, 104 insertions(+) create mode 100644 test/common/antlr_interval_set_test.cpp diff --git a/test/common/CMakeLists.txt b/test/common/CMakeLists.txt index 0abff0dd1e..b5192c4205 100644 --- a/test/common/CMakeLists.txt +++ b/test/common/CMakeLists.txt @@ -15,4 +15,5 @@ add_lbug_test(types_test vfs_test.cpp boolean_filter_test.cpp simd_filter_test.cpp + antlr_interval_set_test.cpp ) diff --git a/test/common/antlr_interval_set_test.cpp b/test/common/antlr_interval_set_test.cpp new file mode 100644 index 0000000000..88c45e42f0 --- /dev/null +++ b/test/common/antlr_interval_set_test.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "misc/IntervalSet.h" + +using antlr4::misc::Interval; +using antlr4::misc::IntervalSet; + +namespace { + +constexpr ssize_t RANGE = 512; + +void checkMatches(const std::vector& added) { + IntervalSet set; + std::bitset expected; + for (auto& interval : added) { + set.add(interval); + for (auto v = interval.a; v <= interval.b; v++) { + expected.set(v); + } + } + for (ssize_t v = 0; v < RANGE; v++) { + ASSERT_EQ(set.contains(v), expected.test(v)) << v; + } + // Canonical form: sorted, and neither overlapping nor touching. + auto& intervals = set.getIntervals(); + for (auto i = 1u; i < intervals.size(); i++) { + ASSERT_GT(intervals[i].a, intervals[i - 1].b + 1); + } +} + +} // namespace + +TEST(AntlrIntervalSetTest, AddKeepsCanonicalForm) { + std::mt19937 rng(42); + std::uniform_int_distribution start(0, RANGE - 20), length(0, 6); + for (auto round = 0; round < 200; round++) { + std::vector added; + for (auto i = 0; i < 40; i++) { + auto a = start(rng); + added.emplace_back(a, a + length(rng)); + } + checkMatches(added); + std::sort(added.begin(), added.end(), + [](const Interval& l, const Interval& r) { return l.a < r.a; }); + checkMatches(added); + } + // Sorted input that touches or overlaps the last interval: [1,3] and [4,6] merge, [8,10] is + // appended, and [10,12] merges with it. + std::vector touching; + for (ssize_t a : {1, 4, 8, 10}) { + touching.emplace_back(a, a + 2); + } + checkMatches(touching); +} + +// The deserializer adds EOF (-1) first: a range starting at 0 must merge with it, one starting +// at 1 must not. +TEST(AntlrIntervalSetTest, EofMergesOnlyWithTouchingRange) { + IntervalSet merged; + merged.add(-1); + merged.add(0, 2); + ASSERT_EQ(merged.getIntervals().size(), 1u); + EXPECT_EQ(merged.getIntervals()[0], + Interval(static_cast(-1), static_cast(2))); + IntervalSet split; + split.add(-1); + split.add(1, 2); + ASSERT_EQ(split.getIntervals().size(), 2u); + EXPECT_EQ(split.getIntervals()[0], + Interval(static_cast(-1), static_cast(-1))); + EXPECT_EQ(split.getIntervals()[1], Interval(static_cast(1), static_cast(2))); +} + +// An empty interval is ignored, and the fast path's comparisons hold at the top of the range. +// Each step keeps ANTLR's adjacent() checks below SSIZE_MAX, where they would overflow. +TEST(AntlrIntervalSetTest, EmptyIntervalAndRangeLimits) { + constexpr auto MAX = std::numeric_limits::max(); + IntervalSet set; + set.add(5, 3); + EXPECT_TRUE(set.getIntervals().empty()); + set.add(MAX - 3); + set.add(MAX - 1); + ASSERT_EQ(set.getIntervals().size(), 2u); + set.add(MAX - 2); + ASSERT_EQ(set.getIntervals().size(), 1u); + set.add(MAX); + ASSERT_EQ(set.getIntervals().size(), 1u); + EXPECT_EQ(set.getIntervals()[0], Interval(MAX - 3, MAX)); +} diff --git a/third_party/antlr4_runtime/src/misc/IntervalSet.cpp b/third_party/antlr4_runtime/src/misc/IntervalSet.cpp index 058d98703e..be5ad8099b 100755 --- a/third_party/antlr4_runtime/src/misc/IntervalSet.cpp +++ b/third_party/antlr4_runtime/src/misc/IntervalSet.cpp @@ -66,6 +66,15 @@ void IntervalSet::add(const Interval &addition) { return; } + // Ladybug local patch on ANTLR 4.13.1, which has no such fast path; keep it on a runtime + // upgrade. Deserialized sets arrive sorted; appending them one by one would otherwise scan the + // whole list per interval. + if (_intervals.empty() || + (_intervals.back().b < addition.a && !addition.adjacent(_intervals.back()))) { + _intervals.push_back(addition); + return; + } + // find position in list for (auto iterator = _intervals.begin(); iterator != _intervals.end(); ++iterator) { Interval r = *iterator;