From 267b77c9942650b680f2bd6a484b18c2d99269f4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 01:08:13 -0500 Subject: [PATCH] Partition programs with multi-line dispatch tables as text The emitter writes interface dispatch tables one element per line. The helper's text partitioner accepted only single-line globals, so any program with such a table (every partitioned program with class interfaces) fell back to parsing the whole module before splitting it with LLVM. The text partitioner now reads a global whose aggregate initializer opens at the end of its line and closes on a line that holds only the closing bracket. Those programs now parse their partitions concurrently and keep the text partitioner's placement, so an edit to one function changes only that function's partition again. --- native/llvm-codegen/src/partition.cpp | 29 ++++++++++++++++++- .../compiler/test/native-partitions.test.ts | 25 ++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/native/llvm-codegen/src/partition.cpp b/native/llvm-codegen/src/partition.cpp index 8ad696d33..f4add0e6d 100644 --- a/native/llvm-codegen/src/partition.cpp +++ b/native/llvm-codegen/src/partition.cpp @@ -359,7 +359,34 @@ std::optional partitionProgramText(StringRef Text, } else if (Line.front() == '@') { Symbol S; S.Header = Line; - if (!parseGlobal(Line, S) || !Add(std::move(S))) + if (!parseGlobal(Line, S)) + return std::nullopt; + // The emitter writes an interface dispatch table one element per + // line: the definition opens its aggregate initializer at the end of + // the line, and a line that is only the closing bracket ends it. + StringRef Opening = Line.rtrim(); + if (S.Kind == SymbolKind::Variable && + (Opening.ends_with(" [") || Opening.ends_with(" {"))) { + StringRef Close = Opening.back() == '[' ? "]" : "}"; + size_t Cursor = Next; + while (true) { + if (Cursor >= Text.size()) + return std::nullopt; + size_t LineEnd = Text.find('\n', Cursor); + if (LineEnd == StringRef::npos) + LineEnd = Text.size(); + StringRef Element = Text.slice(Cursor, LineEnd); + if (Element == Close) { + S.Body = Text.slice(Next, LineEnd); + Next = LineEnd + 1; + break; + } + if (!Element.starts_with(" ")) + return std::nullopt; + Cursor = LineEnd + 1; + } + } + if (!Add(std::move(S))) return std::nullopt; } else if (Line.starts_with("declare ")) { Symbol S; diff --git a/packages/compiler/test/native-partitions.test.ts b/packages/compiler/test/native-partitions.test.ts index cdccb433c..ce1f14335 100644 --- a/packages/compiler/test/native-partitions.test.ts +++ b/packages/compiler/test/native-partitions.test.ts @@ -162,6 +162,31 @@ describe.runIf(helper !== null)("LLVM program partitions", () => { }); } + test("a table written one element per line keeps the text partitioner and its reuse", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-partitions-table-")); + dirs.push(dir); + // The emitter writes interface dispatch tables one element per line. + const table = [ + "@table = internal constant [2 x ptr] [", + " ptr @step3,", + " ptr @step9", + "]", + ].join("\n"); + const source = (edit: number) => + program(edit, table).replace( + " %total = load i64, ptr @total", + " %entry = load ptr, ptr @table\n call void %entry(i64 0)\n %total = load i64, ptr @total", + ); + const cache = join(dir, "cache"); + const first = await emit(dir, source(0), { outputs: 4, optLevel: "2", cache, name: "first" }); + expect(await exitCode(dir, first, "first")).toBe(expectedExit()); + const sources = await cacheEntries(cache, ".bc"); + const edited = await emit(dir, source(7), { outputs: 4, optLevel: "2", cache, name: "edited" }); + const after = await cacheEntries(cache, ".bc"); + expect([...after].filter((entry) => !sources.has(entry))).toHaveLength(1); + expect(await exitCode(dir, edited, "edited")).toBe(expectedExit(7)); + }); + test("inputs outside the text partitioner's grammar are still partitioned", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-partitions-fallback-")); dirs.push(dir);