Skip to content

Commit 0b08887

Browse files
authored
Merge pull request #398 from on-keyday/gen/ebmcodegen-shared-body
gen: ebmcodegen emits shared body + thin wrappers (ADR 0047)
2 parents 37b53f4 + 7050e4d commit 0b08887

64 files changed

Lines changed: 167391 additions & 1502355 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# ebmcodegen の per-lang 生成物は共有 body + thin wrapper で emit する
2+
3+
## 日付
4+
5+
- 判断時期: 2026-07-29
6+
- 文書化: 2026-07-29
7+
8+
## 判断
9+
10+
ebmcodegen が各 `ebm2<lang>` に生成していた `main.cpp` (77k 行) / `codegen.hpp`
11+
(6.5k 行) の言語別フルコピー (18 生成器 × 約 83.5k 行 ≒ 150 万行) を廃止し、
12+
13+
- **共有 body**: `src/ebmcodegen/generated/class_{codegen,interpret}_{source,header}.inc`
14+
に言語非依存の本体を 1 部だけ生成する。名前空間は `CODEGEN_NAMESPACE` マクロで
15+
パラメータ化する。
16+
- **thin wrapper**: 各言語の `main.cpp` / `codegen.hpp`
17+
`#define CODEGEN_NAMESPACE ebm2<lang>` + `#define CODEGEN_LANG_NAME "<lang>"` +
18+
共有 body の `#include` だけの数行に置き換える。
19+
20+
ツール側は `--mode <class-mode>-{wrapper,body}` の GenerateMode 追加で出し分ける
21+
(成果物の種類はフラグでなく mode で表す既存規約に従う)。
22+
23+
## 動機
24+
25+
- 従来方式は「言語名トークン置換以外テキスト同一」のファイルを 18 部コミット
26+
していた (正規化 diff で main.cpp は差分ゼロ、codegen.hpp は
27+
`lang_name = "<lang>"` / `file_extensions` の 2 行のみ)。EBM 構造変更のたびに
28+
150 万行が再生成され、git churn・grep ノイズ・レビュー対象の膨張を招いていた。
29+
- フック側が消費するインターフェース (`DEFINE_VISITOR`, `CODEGEN_VISITOR(name)`,
30+
`CODEGEN_NAMESPACE`) は既に全てマクロ間接化されており、パラメータ化の障害が
31+
なかった。生成テキスト側だけが言語名を ~9,700 箇所ハードコードしていた。
32+
- 本変更で追跡対象の生成物は共有 body ~167k 行 + wrapper 18×7 行になる
33+
(約 94% 削減)。
34+
35+
## 具体例
36+
37+
- wrapper (`src/ebmcg/ebm2go/main.cpp`):
38+
39+
```cpp
40+
#define CODEGEN_NAMESPACE ebm2go
41+
#define CODEGEN_LANG_NAME "go"
42+
#include <ebmcodegen/generated/class_codegen_source.inc>
43+
```
44+
45+
- 共有 body 内の per-hook ブロックは `CODEGEN_NAMESPACE::Visitor<...>` を使い、
46+
文字列リテラルに言語名が要る箇所 (BaseVisitor::program_name,
47+
TEMPORARY_CHECK_MACRO の期待文字列, webworker_name) は
48+
`EBMCG_STRINGIFY(CODEGEN_NAMESPACE)` (stub/util.hpp で定義) で構成する。
49+
期待文字列の分割結合が展開後 stringify と一致することはコンパイル実験で確認済み。
50+
- フック発見の `__has_include("visitor/<hook>_class.hpp")` は「ファイル相対」から
51+
「per-target include path」に移行: 生成 CMakeLists が
52+
`target_include_directories(ebm2<lang> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})`
53+
を持ち、同一 body が言語ごとに異なるフック集合を拾う。
54+
- `ebmtemplate.py` の「新フック追加時に main.cpp を touch」ワークフローは
55+
wrapper が main.cpp なので無変更で機能し、再ビルド粒度も per-lang のまま。
56+
- `script/ebmcodegen.py` は全出力を write-if-changed にし、body 不変時の
57+
無駄な全言語再ビルドを避ける。
58+
59+
## 制約 (quote-include の探索順序)
60+
61+
quote-include は「その directive を含むファイルのディレクトリ」を最優先で探索
62+
するため、**共有 body の置き場所に `visitor/` サブディレクトリを置いてはならない**。
63+
置くと全言語がそのフックを言語自身の `visitor/` より優先して拾う (clang 22 で
64+
実証済み)。`src/ebmcodegen/generated/` はこの不変条件を守る専用ディレクトリで
65+
あり、`default_codegen_visitor/` 直下 (visitor/ を持つ) には置けない。
66+
67+
## これは X を意味しない
68+
69+
- **コンパイル時間の削減ではない**。18 TU がそれぞれ共有 body 全体を前処理・
70+
インスタンス化する構図は不変 (PCH は従来から codegen_pch で共有)。削るのは
71+
リポジトリ上の重複テキストと regen churn である。
72+
- **名前空間の統一ではない**。`ebm2go::` 等の per-lang 名前空間は維持される
73+
(wrapper が解決)。単一名前空間化は clangd のインデックスで 18 言語の同名
74+
特殊化が混線するため却下した。
75+
- 非クラスベースのレガシー `codegen` モード、`ebmgen-visitor` モード、
76+
`default_codegen_visitor/codegen.hpp` (ebm2all IDE 用 dummy header) は
77+
従来どおり非パラメータ化 emission のまま。
78+
79+
## 代替案
80+
81+
- **単一固定名前空間 + 完全同一ファイル**: wrapper すら不要になるが、追加削減は
82+
僅かで clangd の cross-lang jump 混線という IDE 体験悪化が upside に見合わない。
83+
- **存在するフックのみ emit**: 唯一コンパイル時間に効く案だが、フック追加ごとに
84+
main.cpp 再生成が必要になり `__has_include` による動的フック発見 (touch のみで
85+
反映) のワークフローを壊すため見送り。
86+
- **フラグ (--wrapper/--shared-body) での出し分け**: mode と直交するフラグは
87+
無効組合せの検証を増やすだけで、「成果物の種類 = mode」の既存規約に反するため
88+
GenerateMode 追加に変更した。

rebrgen/script/ebmcodegen.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,53 @@
1414
TOOL_PATH += ".exe"
1515
TOOL_PATH = os.path.abspath(TOOL_PATH)
1616

17+
SHARED_BODY_DIR = "src/ebmcodegen/generated"
18+
19+
20+
def write_if_changed(path: str, content: bytes) -> bool:
21+
"""Write content to path only when it differs; returns True when written.
22+
23+
Keeping mtimes stable matters here: ninja rebuilds every ebm2<lang> target
24+
whose (wrapper) main.cpp or the shared body changed.
25+
"""
26+
if os.path.exists(path):
27+
with open(path, "rb") as f:
28+
if f.read() == content:
29+
return False
30+
with open(path, "wb") as f:
31+
f.write(content)
32+
return True
33+
34+
35+
_shared_bodies_done: set = set()
36+
37+
38+
def ensure_shared_bodies(mode: str):
39+
"""Generate the language-agnostic shared bodies included by per-language wrappers."""
40+
if mode in _shared_bodies_done:
41+
return
42+
_shared_bodies_done.add(mode)
43+
isInterpreter = mode == "interpret" or mode == "interpret-class"
44+
family = "interpret" if isInterpreter else "codegen"
45+
DEFAULT_VISITOR_LOCATION = f"ebmcodegen/default_{family}_visitor/visitor/"
46+
os.makedirs(SHARED_BODY_DIR, exist_ok=True)
47+
for kind in ("header", "source"):
48+
content = execute(
49+
[
50+
TOOL_PATH,
51+
"--mode",
52+
f"{mode}-{kind}-body",
53+
"--default-visitor-impl-dir",
54+
DEFAULT_VISITOR_LOCATION,
55+
],
56+
None,
57+
)
58+
path = os.path.join(SHARED_BODY_DIR, f"class_{family}_{kind}.inc")
59+
if write_if_changed(path, content):
60+
print(f"Generated shared body: {path}")
61+
else:
62+
print(f"Shared body is up to date: {path}")
63+
1764

1865
def do_default_dummy_header(lang_name: str, mode: str):
1966
isInterpreter = mode == "interpret" or mode == "interpret-class"
@@ -74,30 +121,26 @@ def do_setup(lang_name: str, mode: str, file_extension: str):
74121

75122
CMAKE = execute([TOOL_PATH, "--mode", "cmake", "--lang", lang_name], None)
76123

77-
DEFAULT_VISITOR_LOCATION = f"ebmcodegen/default_{"interpret" if isInterpreter else "codegen"}_visitor/visitor/"
78-
79124
if isClassBased:
125+
# per-language files are thin wrappers over the shared bodies in src/ebmcodegen/generated/
126+
ensure_shared_bodies(mode)
80127
CODE_GENERATOR_HEADER = execute(
81128
[
82129
TOOL_PATH,
83130
"--mode",
84-
mode + "-header",
131+
mode + "-header-wrapper",
85132
"--lang",
86133
lang_name,
87-
"--default-visitor-impl-dir",
88-
DEFAULT_VISITOR_LOCATION,
89134
],
90135
None,
91136
)
92137
CODE_GENERATOR = execute(
93138
[
94139
TOOL_PATH,
95140
"--mode",
96-
mode + "-source",
141+
mode + "-source-wrapper",
97142
"--lang",
98143
lang_name,
99-
"--default-visitor-impl-dir",
100-
DEFAULT_VISITOR_LOCATION,
101144
],
102145
None,
103146
)
@@ -109,13 +152,10 @@ def do_setup(lang_name: str, mode: str, file_extension: str):
109152
VISITOR_DIR = os.path.join(OUTPUT_DIR, "visitor")
110153
os.makedirs(VISITOR_DIR, exist_ok=True)
111154

112-
with open(os.path.join(OUTPUT_DIR, "CMakeLists.txt"), "wb") as f:
113-
f.write(CMAKE)
114-
with open(os.path.join(OUTPUT_DIR, "main.cpp"), "wb") as f:
115-
f.write(CODE_GENERATOR)
155+
write_if_changed(os.path.join(OUTPUT_DIR, "CMakeLists.txt"), CMAKE)
156+
write_if_changed(os.path.join(OUTPUT_DIR, "main.cpp"), CODE_GENERATOR)
116157
if CODE_GENERATOR_HEADER is not None:
117-
with open(os.path.join(OUTPUT_DIR, "codegen.hpp"), "wb") as f:
118-
f.write(CODE_GENERATOR_HEADER)
158+
write_if_changed(os.path.join(OUTPUT_DIR, "codegen.hpp"), CODE_GENERATOR_HEADER)
119159

120160
# add FILE_EXTENSIONS(ext) to Flags.hpp using script/ebmtemplate.py
121161
flags_path = os.path.join(VISITOR_DIR, "Flags.hpp")

rebrgen/src/ebmcg/ebm2c/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tool)
55
add_executable(ebm2c
66
"main.cpp"
77
)
8+
# so that the shared body (ebmcodegen/generated/) can find this language's visitor/ hooks via __has_include
9+
target_include_directories(ebm2c PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
810
target_precompile_headers(ebm2c REUSE_FROM codegen_pch)
911
if(UNIX)
1012
set_target_properties(ebm2c PROPERTIES INSTALL_RPATH "${CMAKE_SOURCE_DIR}/tool")

0 commit comments

Comments
 (0)