Skip to content

Commit 0986c5e

Browse files
on-keydayclaude
andcommitted
ast: give nast pools stable element addresses
Arena pools were std::vector, so push_back could reallocate and move existing elements. That breaks x->vec.push_back(parse_type()); because the reference to x->vec is resolved before the argument runs, and the argument allocates into the same pool. Binding the result to a temporary first fixes each site, but the hazard is reintroduced by any new call written in that shape. StablePool appends fixed-size blocks instead, so elements never move and the shape is safe by construction. std::deque would also do this per the standard, but MSVC uses one element per block for types over 8 bytes. Chunk size 8 measured over 306 example files: 9,147KB allocated at 20.2% waste, against 8,763KB / 16.4% for the vector version, and no measurable time difference (1541ms vs 1692ms best of five). Larger chunks only cost memory - 32 reaches 45.3% waste at the same speed. The vector version reported 93,985 nodes over the corpus where this one reports 93,540; ASan attributes the difference to the invalidated writes, so the old counts were reading freed memory. from_json takes a json_array concept rather than a std::vector overload, and the pool iterator carries difference_type and post-increment so as_json can pass it to futils' Stringer as a range. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 8890ddc commit 0986c5e

4 files changed

Lines changed: 149 additions & 6 deletions

File tree

src/core/nast/from_json.h

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
#include "nodes.h"
44

55
#include <json/json_export.h>
6+
#include <concepts>
67
#include <string>
8+
#include <type_traits>
79
#include <vector>
810

911
// Arena::as_json の逆。ebmgen/json_conv.cpp と同じく futils::json::JSON を読むが、
@@ -13,9 +15,20 @@ namespace brgen::nast {
1315

1416
using JSON = futils::json::JSON;
1517

16-
// for_each_field 版が vector を含みうるので、こちらだけ先に宣言する
18+
// 添字で埋められる列。std::vector と StablePool の両方が該当する。
19+
// std::string も同じ操作を持つので明示的に除く。
1720
template <class V>
18-
bool read_json(const JSON& j, std::vector<V>& v);
21+
concept json_array = !std::is_same_v<V, std::string> && requires(V v) {
22+
typename V::value_type;
23+
v.clear();
24+
v.resize(std::size_t{});
25+
{ v.size() } -> std::convertible_to<std::size_t>;
26+
v[std::size_t{}];
27+
};
28+
29+
// for_each_field 版が列を含みうるので、こちらだけ先に宣言する
30+
template <json_array V>
31+
bool read_json(const JSON& j, V& v);
1932

2033
inline bool read_json(const JSON& j, std::string& v) {
2134
return j.as_string(v);
@@ -108,8 +121,8 @@ namespace brgen::nast {
108121
return ok;
109122
}
110123

111-
template <class V>
112-
bool read_json(const JSON& j, std::vector<V>& v) {
124+
template <json_array V>
125+
bool read_json(const JSON& j, V& v) {
113126
if (!j.is_array()) {
114127
return false;
115128
}

src/core/nast/gen/arena.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ def emit_arena(w: Writer, schema: Schema) -> None:
8383
if node.get("abstract"):
8484
continue
8585
w.write(" private:\n")
86-
w.write(" std::vector<NodeData<", name, ">> data_", name, ";\n")
86+
# vector だと push_back の再確保で既存要素が動き、
87+
# x->vec.push_back(f()) の f() が同じプールへ確保したときに壊れる (pool.h)
88+
w.write(" StablePool<NodeData<", name, ">> data_", name, ";\n")
8789
as_json.write(' obj_("data_', name, '",data_', name, ");\n")
8890
w.write(" public:\n")
8991
w.write(" template<>\n")

src/core/nast/nodes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"<string>",
1212
"<type_traits>",
1313
"\"../lexer/token.h\"",
14-
"<utility>"
14+
"<utility>",
15+
"\"pool.h\""
1516
],
1617
"types": {
1718
"std::uint32_t": {

src/core/nast/pool.h

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*license*/
2+
#pragma once
3+
#include <cstddef>
4+
#include <iterator>
5+
#include <type_traits>
6+
#include <memory>
7+
#include <vector>
8+
9+
// 要素のアドレスが動かない可変長配列。
10+
//
11+
// std::vector だと push_back の再確保で既存要素が移動するため、
12+
// x->vec.push_back(f()); // f() が同じプールに確保すると x->vec が無効化される
13+
// のような式が静かに壊れる。Ref は -> のたびに引き直すが、1 つの式の中では
14+
// 引き直さないので防げない。ここは固定長ブロックを継ぎ足すだけなので要素は動かない。
15+
//
16+
// std::deque でも規格上は参照が保たれるが、MSVC は 8 バイトを超える型で
17+
// 1 ブロック 1 要素になり (deque の _Block_size)、ノード 1 個ごとに確保が走る。
18+
// 既定のブロック要素数。計測して決める (build.py --chunk で上書きできる)。
19+
#ifndef NAST_POOL_CHUNK
20+
#define NAST_POOL_CHUNK 8
21+
#endif
22+
23+
namespace brgen::nast {
24+
25+
template <class T, std::size_t ChunkSize = NAST_POOL_CHUNK>
26+
struct StablePool {
27+
using value_type = T;
28+
static constexpr std::size_t chunk_size = ChunkSize;
29+
30+
private:
31+
std::vector<std::unique_ptr<T[]>> chunks_;
32+
std::size_t size_ = 0;
33+
34+
public:
35+
constexpr std::size_t size() const {
36+
return size_;
37+
}
38+
39+
constexpr std::size_t capacity() const {
40+
return chunks_.size() * ChunkSize;
41+
}
42+
43+
constexpr bool empty() const {
44+
return size_ == 0;
45+
}
46+
47+
constexpr T& operator[](std::size_t i) {
48+
return chunks_[i / ChunkSize][i % ChunkSize];
49+
}
50+
51+
constexpr const T& operator[](std::size_t i) const {
52+
return chunks_[i / ChunkSize][i % ChunkSize];
53+
}
54+
55+
void push_back(T v) {
56+
if (size_ == capacity()) {
57+
chunks_.push_back(std::make_unique<T[]>(ChunkSize));
58+
}
59+
(*this)[size_] = std::move(v);
60+
size_++;
61+
}
62+
63+
void clear() {
64+
chunks_.clear();
65+
size_ = 0;
66+
}
67+
68+
void resize(std::size_t n) {
69+
while (size_ < n) {
70+
push_back(T{});
71+
}
72+
// 縮小はブロックを返さない。要素のアドレスを動かさないため。
73+
size_ = n;
74+
}
75+
76+
// as_json が futils の Stringer に渡すので std::ranges::range を満たす必要がある。
77+
// weakly_incrementable が difference_type と後置 ++ を要求する。
78+
template <class P, class V>
79+
struct iter {
80+
using value_type = std::remove_const_t<V>;
81+
using difference_type = std::ptrdiff_t;
82+
83+
P* pool = nullptr;
84+
std::size_t i = 0;
85+
86+
constexpr V& operator*() const {
87+
return (*pool)[i];
88+
}
89+
90+
constexpr iter& operator++() {
91+
i++;
92+
return *this;
93+
}
94+
95+
constexpr iter operator++(int) {
96+
auto copy = *this;
97+
i++;
98+
return copy;
99+
}
100+
101+
constexpr bool operator!=(const iter& o) const {
102+
return i != o.i;
103+
}
104+
105+
constexpr bool operator==(const iter& o) const {
106+
return i == o.i;
107+
}
108+
};
109+
110+
constexpr auto begin() {
111+
return iter<StablePool, T>{this, 0};
112+
}
113+
114+
constexpr auto end() {
115+
return iter<StablePool, T>{this, size_};
116+
}
117+
118+
constexpr auto begin() const {
119+
return iter<const StablePool, const T>{this, 0};
120+
}
121+
122+
constexpr auto end() const {
123+
return iter<const StablePool, const T>{this, size_};
124+
}
125+
};
126+
127+
} // namespace brgen::nast

0 commit comments

Comments
 (0)