Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
luau_package.cpp
Go to the documentation of this file.
1// luau_package.cpp — パッケージの読み込み・順番・require の解決・表の合わせ方(T-0138、13 §2・ADR-0031)。
2//
3// 流れ: マニフェストを全部読む(名前の順)→ 依存で順番を決める(決まらない所は名前の順)→ 順番に entry を走らせる
4// → 戻り値の「分類 → 鍵 → 値」を確かめる → MergeMode で合わせる。
5// パッケージの誤り(マニフェスト・依存・実行・上書きの禁止)はそのパッケージと、それに依存するものだけを読まない(ほかは続ける)。
6// Luau は全部 1 つの殻で走らせる(Run ごとにグローバルは別。13 §2.1)。
8
9#include <algorithm>
10#include <format>
11#include <fstream>
12#include <iterator>
13#include <set>
14#include <utility>
15
16#include "script/luau_sandbox.h"
17
19
20 namespace {
21
22 constexpr std::string_view MANIFEST_FILE = "package.luau";
23 constexpr std::string_view MODULE_EXTENSION = ".luau";
24 constexpr size_t MAX_NAME_LENGTH = 64;
25 constexpr size_t MAX_LISTED_CONFLICTS = 3;
26
27 struct Manifest {
28 std::vector<std::string> depends; // basePackage も入れたもの
29 std::string entry = "init";
30 };
31
32 // 分類 → 鍵 → 値(1 つのパッケージが返したもの)
33 using Contribution = std::map<std::string, std::map<std::string, ScriptValue>>;
34
35 std::string ToUtf8String(const fs::path& path) {
36 const std::u8string text = path.generic_u8string();
37
38 return {text.begin(), text.end()};
39 }
40
41 // --- require の名前 → パッケージの中のファイル ---
42
43 // "sub/util" → "sub/util.luau"。区切りごとに名前の規則(".." や絶対パス・拡張子つきは通らない)
44 std::expected<std::string, std::string> ModulePath(std::string_view name) {
45 size_t begin = 0;
46 while (true) {
47 const size_t slash = name.find('/', begin);
48 const std::string_view segment = name.substr(begin,
49 slash == std::string_view::npos ? slash : slash - begin);
50 if (!IsValidPackageName(segment))
51 return std::unexpected(
52 "名前は 'sub/util' の形(英小文字・数字・_・- を / で区切る。.. や拡張子は書かない)");
53 if (slash == std::string_view::npos)
54 break;
55
56 begin = slash + 1;
57 }
58
59 return std::string(name) + std::string(MODULE_EXTENSION);
60 }
61
63 return [&package](std::string_view name) -> std::expected<ModuleFile, std::string> {
64 auto path = ModulePath(name);
65 if (!path)
66 return std::unexpected(path.error());
67
68 const auto found = package.files.find(*path);
69 if (found == package.files.end())
70 return std::unexpected(std::format("パッケージ '{}' に {} が無い", package.name, *path));
71
72 return ModuleFile{.chunkName = package.name + "/" + *path, .source = found->second};
73 };
74 }
75
76 // --- マニフェスト ---
77
78 std::expected<std::vector<std::string>, std::string> ReadNameList(const ScriptValue& list) {
79 if (!list.IsTable())
80 return std::unexpected("depends は名前の配列");
81
82 std::vector<std::string> names;
83 for (const ScriptField& field : list.fields) {
84 const bool inSequence = field.key.IsNumber() &&
85 field.key.number == static_cast<double>(names.size() + 1);
86 if (!inSequence || !field.value.IsString() || !IsValidPackageName(field.value.text))
87 return std::unexpected("depends は名前の配列({ \"core\", \"other\" } の形)");
88
89 names.push_back(field.value.text);
90 }
91
92 return names;
93 }
94
95 std::expected<Manifest, std::string> ParseManifest(const ScriptValue& table) {
96 if (!table.IsTable())
97 return std::unexpected("package.luau は表を 1 つ返す");
98
99 Manifest manifest;
100 bool hasFormat = false;
101 for (const ScriptField& field : table.fields) {
102 const std::string_view key = field.key.IsString() ? std::string_view(field.key.text) : "";
103 if (key == "format") {
104 if (!field.value.IsNumber() || field.value.number != static_cast<double>(PACKAGE_FORMAT_VERSION))
105 return std::unexpected(std::format("format は {}(この版が読める形)", PACKAGE_FORMAT_VERSION));
106 hasFormat = true;
107 } else if (key == "depends") {
108 auto names = ReadNameList(field.value);
109 if (!names)
110 return std::unexpected(names.error());
111 manifest.depends = std::move(*names);
112 } else if (key == "entry") {
113 if (!field.value.IsString() || !ModulePath(field.value.text))
114 return std::unexpected("entry はパッケージの中のファイルの名前('init' の形)");
115 manifest.entry = field.value.text;
116 } else {
117 return std::unexpected(
118 std::format("知らない欄 {}(書けるのは format・depends・entry)", ToDebugText(field.key)));
119 }
120 }
121
122 if (!hasFormat)
123 return std::unexpected(std::format("format = {} が無い", PACKAGE_FORMAT_VERSION));
124
125 return manifest;
126 }
127
128 std::expected<Manifest, std::string> ReadManifest(LuauSandbox& sandbox, const PackageSource& package,
129 const PackageLoadOptions& options) {
130 const auto file = package.files.find(std::string(MANIFEST_FILE));
131 if (file == package.files.end())
132 return std::unexpected(std::format("{} が無い", MANIFEST_FILE));
133
134 const auto run = sandbox.Run(package.name + "/" + std::string(MANIFEST_FILE), file->second,
135 RunOptions{.modules = nullptr, .captureValues = true});
136 if (!run)
137 return std::unexpected(run.error().message);
138 if (run->values.size() != 1)
139 return std::unexpected("package.luau は表を 1 つ返す");
140
141 auto manifest = ParseManifest(run->values[0]);
142 if (manifest && !options.basePackage.empty() && package.name != options.basePackage &&
143 std::ranges::find(manifest->depends, options.basePackage) == manifest->depends.end())
144 manifest->depends.push_back(options.basePackage);
145
146 return manifest;
147 }
148
149 // --- 順番(依存の後。決まらない所は名前の順)---
150
151 enum class OrderState : uint8_t {
155 };
156
161
162 // 依存のうち、まだ並べられていないもの(無い・読まないものなら理由も)
163 bool DependsReady(const std::map<std::string, OrderNode>& nodes, const OrderNode& node,
164 std::string* missingReason) {
165 for (const std::string& depend : node.manifest.depends) {
166 const auto found = nodes.find(depend);
167 if (found == nodes.end()) {
168 if (missingReason)
169 *missingReason = std::format("依存するパッケージ '{}' が無い", depend);
170 return false;
171 }
172 if (found->second.state == OrderState::Rejected) {
173 if (missingReason)
174 *missingReason = std::format("依存するパッケージ '{}' を読まなかった", depend);
175 return false;
176 }
177 if (found->second.state != OrderState::Ordered)
178 return false;
179 }
180
181 return true;
182 }
183
184 // 1 つ進める: 並べられる最小の名前を並べる → 依存が欠けたものを読まない → 残りは循環。進めなければ false
185 bool OrderStep(std::map<std::string, OrderNode>& nodes, std::vector<std::string>& order,
186 PackageSetResult& result) {
187 for (auto& [name, node] : nodes) {
188 if (node.state == OrderState::Pending && DependsReady(nodes, node, nullptr)) {
189 node.state = OrderState::Ordered;
190 order.push_back(name);
191
192 return true;
193 }
194 }
195
196 for (auto& [name, node] : nodes) {
197 std::string reason;
198 if (node.state == OrderState::Pending && !DependsReady(nodes, node, &reason) && !reason.empty()) {
199 node.state = OrderState::Rejected;
200 result.rejected.push_back(PackageRejection{.package = name, .reason = std::move(reason)});
201
202 return true;
203 }
204 }
205
206 bool anyPending = false;
207 for (auto& [name, node] : nodes) {
208 if (node.state != OrderState::Pending)
209 continue;
210
211 node.state = OrderState::Rejected;
212 result.rejected.push_back(PackageRejection{.package = name, .reason = "依存が循環している"});
213 anyPending = true;
214 }
215
216 return anyPending;
217 }
218
219 // --- entry の戻り値 → 分類 → 鍵 → 値 ---
220
221 std::expected<Contribution, std::string> ReadContribution(const std::vector<ScriptValue>& values) {
222 Contribution contribution;
223 if (values.empty() || values[0].kind == ScriptValue::Kind::Nil)
224 return contribution; // 何も足さないパッケージ(ほかのパッケージの道具だけなど)
225 if (values.size() != 1 || !values[0].IsTable())
226 return std::unexpected("entry は「分類 → 鍵 → 値」の表を 1 つ返す");
227
228 for (const ScriptField& category : values[0].fields) {
229 if (!category.key.IsString() || !IsValidPackageName(category.key.text) || !category.value.IsTable())
230 return std::unexpected(
231 std::format("分類 {} は名前(英小文字・数字・_・-)で、値は表", ToDebugText(category.key)));
232
233 auto& entries = contribution[category.key.text];
234 for (const ScriptField& entry : category.value.fields) {
235 if (!entry.key.IsString() || entry.key.text.empty())
236 return std::unexpected(
237 std::format("{} の鍵 {} は空でない文字列", category.key.text, ToDebugText(entry.key)));
238
239 entries.emplace(entry.key.text, entry.value);
240 }
241 }
242
243 return contribution;
244 }
245
246 std::expected<Contribution, std::string> RunEntry(LuauSandbox& sandbox, const PackageSource& package,
247 const Manifest& manifest) {
248 const std::string path = manifest.entry + std::string(MODULE_EXTENSION);
249 const auto file = package.files.find(path);
250 if (file == package.files.end())
251 return std::unexpected(std::format("entry の {} が無い", path));
252
253 const ModuleResolver resolver = MakeResolver(package);
254 const auto run = sandbox.Run(package.name + "/" + path, file->second,
255 RunOptions{.modules = &resolver, .captureValues = true});
256 if (!run)
257 return std::unexpected(run.error().message);
258
259 return ReadContribution(run->values);
260 }
261
262 // --- 合わせる ---
263
264 // AddOnly で上書きがあれば理由を返す(合わせない)
265 std::string FindForbiddenOverrides(const PackageSetResult& result, const Contribution& contribution) {
266 std::vector<std::string> conflicts;
267 for (const auto& [category, entries] : contribution) {
268 const auto existing = result.tables.find(category);
269 if (existing == result.tables.end())
270 continue;
271
272 for (const auto& [key, value] : entries) {
273 const auto previous = existing->second.find(key);
274 if (previous != existing->second.end())
275 conflicts.push_back(
276 std::format("{}.{}('{}' が書いた)", category, key, previous->second.package));
277 }
278 }
279
280 if (conflicts.empty())
281 return {};
282
283 std::string reason = "既にある鍵を上書きしようとした(追加だけの設定): ";
284 for (size_t index = 0; index < conflicts.size() && index < MAX_LISTED_CONFLICTS; ++index)
285 reason += (index > 0 ? "、" : "") + conflicts[index];
286 if (conflicts.size() > MAX_LISTED_CONFLICTS)
287 reason += std::format(" ほか {} 件", conflicts.size() - MAX_LISTED_CONFLICTS);
288
289 return reason;
290 }
291
292 // 1 つの鍵を合わせる(既にあれば上書きを記録する。AddOnly の禁止は FindForbiddenOverrides が先に見ている)
293 void MergeEntry(PackageSetResult& result, const std::string& category, const std::string& key,
294 ScriptValue&& value, const std::string& package, MergeMode mode) {
295 auto [slot, added] = result.tables[category].try_emplace(key);
296 if (!added) {
297 result.overrides.push_back(TableOverride{
298 .category = category, .key = key, .previousPackage = slot->second.package, .package = package});
299 if (mode == MergeMode::OverrideMarked)
300 result.modifiedWorld = true;
301 }
302
303 slot->second = TableEntry{.value = std::move(value), .package = package};
304 }
305
306 void Merge(PackageSetResult& result, const std::string& package, Contribution&& contribution, MergeMode mode) {
307 for (auto& [category, entries] : contribution) {
308 for (auto& [key, value] : entries)
309 MergeEntry(result, category, key, std::move(value), package, mode);
310 }
311 }
312
313 // 1 つのパッケージを読んで合わせる。読まなかったら理由
314 // 型検査の誤りを結果に足し、パッケージを読まない理由を返す(誤りが無ければ空)
315 std::string TypeCheckReason(std::vector<TypeDiagnostic>&& diagnostics, PackageSetResult& result) {
316 if (diagnostics.empty())
317 return {};
318
319 std::string reason = "型検査: " + SummarizeDiagnostics(diagnostics);
320 result.typeDiagnostics.insert(result.typeDiagnostics.end(), std::make_move_iterator(diagnostics.begin()),
321 std::make_move_iterator(diagnostics.end()));
322
323 return reason;
324 }
325
326 std::string LoadOne(LuauSandbox& sandbox, const PackageSource& package, const Manifest& manifest,
327 const std::set<std::string>& loaded, const PackageLoadOptions& options,
328 PackageSetResult& result) {
329 for (const std::string& depend : manifest.depends) {
330 if (!loaded.contains(depend))
331 return std::format("依存するパッケージ '{}' を読まなかった", depend);
332 }
333
334 // --- 走らせる前に型検査(T-0140)---
335 if (options.typeChecker != nullptr) {
336 std::string reason = TypeCheckReason(options.typeChecker->CheckPackage(package, manifest.entry),
337 result);
338 if (!reason.empty())
339 return reason;
340 }
341
342 auto contribution = RunEntry(sandbox, package, manifest);
343 if (!contribution)
344 return contribution.error();
345
346 if (options.mergeMode == MergeMode::AddOnly) {
347 std::string forbidden = FindForbiddenOverrides(result, *contribution);
348 if (!forbidden.empty())
349 return forbidden;
350 }
351
352 Merge(result, package.name, std::move(*contribution), options.mergeMode);
353
354 return {};
355 }
356
357 } // namespace
358
359 // --- 名前 ---
360
361 bool IsValidPackageName(std::string_view name) {
362 if (name.empty() || name.size() > MAX_NAME_LENGTH)
363 return false;
364
365 return std::ranges::all_of(name, [](char character) {
366 return (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') ||
367 character == '_' || character == '-';
368 });
369 }
370
371 // --- ディスクから読む ---
372
373 std::expected<std::string, std::string> PackageModulePath(std::string_view name) {
374 return ModulePath(name);
375 }
376
377 std::expected<PackageSource, std::string> ReadPackageFolder(const fs::path& folder) {
378 std::error_code error;
379 if (!fs::is_directory(folder, error))
380 return std::unexpected(std::format("{} はフォルダではない", ToUtf8String(folder)));
381
382 PackageSource package;
383 package.name = ToUtf8String(folder.filename());
384
385 fs::recursive_directory_iterator iterator(folder, error);
386 for (; !error && iterator != fs::recursive_directory_iterator(); iterator.increment(error)) {
387 const fs::path& path = iterator->path();
388 if (!iterator->is_regular_file(error) || path.extension() != MODULE_EXTENSION)
389 continue;
390
391 std::ifstream file(path, std::ios::binary);
392 if (!file)
393 return std::unexpected(std::format("{} を開けない", ToUtf8String(path)));
394
395 std::string source{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
396 package.files.emplace(ToUtf8String(path.lexically_relative(folder)), std::move(source));
397 }
398
399 if (error)
400 return std::unexpected(std::format("{} を読めない: {}", ToUtf8String(folder), error.message()));
401
402 return package;
403 }
404
405 std::expected<std::vector<PackageSource>, std::string> ReadPackageRoot(const fs::path& root) {
406 std::error_code error;
407 std::vector<fs::path> folders;
408 for (fs::directory_iterator iterator(root, error); !error && iterator != fs::directory_iterator();
409 iterator.increment(error)) {
410 if (iterator->is_directory(error))
411 folders.push_back(iterator->path());
412 }
413
414 if (error)
415 return std::unexpected(std::format("{} を読めない: {}", ToUtf8String(root), error.message()));
416
417 std::ranges::sort(folders);
418
419 std::vector<PackageSource> packages;
420 for (const fs::path& folder : folders) {
421 auto package = ReadPackageFolder(folder);
422 if (!package)
423 return std::unexpected(package.error());
424
425 packages.push_back(std::move(*package));
426 }
427
428 return packages;
429 }
430
431 // --- 読み込みと合わせ ---
432
433 std::expected<PackageSetResult, std::string> LoadPackages(LuauSandbox& sandbox,
434 std::span<const PackageSource> packages,
435 const PackageLoadOptions& options) {
436 if (!sandbox.IsSealed())
437 return std::unexpected("殻を Seal してから読む");
438
439 // --- 名前で引けるように(入れた順に依らない)---
440 std::map<std::string, const PackageSource*> byName;
441 for (const PackageSource& package : packages) {
442 if (!byName.emplace(package.name, &package).second)
443 return std::unexpected(std::format("パッケージの名前 '{}' が 2 つある", package.name));
444 }
445
446 PackageSetResult result;
447
448 // --- マニフェスト(名前の順)---
449 std::map<std::string, OrderNode> nodes;
450 for (const auto& [name, package] : byName) {
451 if (options.typeChecker != nullptr && IsValidPackageName(name)) {
452 std::string reason = TypeCheckReason(options.typeChecker->CheckManifest(*package), result);
453 if (!reason.empty()) {
454 result.rejected.push_back(PackageRejection{.package = name, .reason = std::move(reason)});
455 continue;
456 }
457 }
458
459 auto manifest = IsValidPackageName(name)
460 ? ReadManifest(sandbox, *package, options)
461 : std::expected<Manifest, std::string>(std::unexpect,
462 "パッケージの名前は英小文字・数字・_・- だけ");
463 if (!manifest) {
464 result.rejected.push_back(PackageRejection{.package = name, .reason = manifest.error()});
465 continue;
466 }
467
468 nodes.emplace(name, OrderNode{.manifest = std::move(*manifest)});
469 }
470
471 // --- 順番 → 読み込み ---
472 std::vector<std::string> order;
473 while (OrderStep(nodes, order, result)) {
474 }
475
476 std::set<std::string> loaded;
477 for (const std::string& name : order) {
478 std::string reason = LoadOne(sandbox, *byName.at(name), nodes.at(name).manifest, loaded, options, result);
479 if (!reason.empty()) {
480 result.rejected.push_back(PackageRejection{.package = name, .reason = std::move(reason)});
481 continue;
482 }
483
484 loaded.insert(name);
485 result.loadOrder.push_back(name);
486 }
487
488 return result;
489 }
490
491 std::string TableBytes(const PackageSetResult& result) {
492 std::string bytes;
493 AppendCanonicalString("bicameral-tables", bytes);
494 AppendCanonicalString(std::to_string(PACKAGE_FORMAT_VERSION), bytes);
495
496 for (const auto& [category, entries] : result.tables) {
497 AppendCanonicalString(category, bytes);
498 AppendCanonicalString(std::to_string(entries.size()), bytes);
499 for (const auto& [key, entry] : entries) {
500 AppendCanonicalString(key, bytes);
501 AppendCanonicalBytes(entry.value, bytes);
502 }
503 }
504
505 return bytes;
506 }
507
508 uint64_t TableVersion(const PackageSetResult& result) {
509 return HashBytes(TableBytes(result));
510 }
511
512} // namespace bicameral::script
std::expected< RunResult, ScriptError > Run(std::string_view chunkName, std::string_view source, const RunOptions &options={})
std::vector< TypeDiagnostic > CheckPackage(const PackageSource &package, std::string_view entry)
std::vector< TypeDiagnostic > CheckManifest(const PackageSource &package)
void Merge(PackageSetResult &result, const std::string &package, Contribution &&contribution, MergeMode mode)
ModuleResolver MakeResolver(const PackageSource &package)
bool DependsReady(const std::map< std::string, OrderNode > &nodes, const OrderNode &node, std::string *missingReason)
std::expected< Manifest, std::string > ReadManifest(LuauSandbox &sandbox, const PackageSource &package, const PackageLoadOptions &options)
std::map< std::string, std::map< std::string, ScriptValue > > Contribution
std::expected< std::vector< std::string >, std::string > ReadNameList(const ScriptValue &list)
std::expected< std::string, std::string > ModulePath(std::string_view name)
std::expected< Contribution, std::string > ReadContribution(const std::vector< ScriptValue > &values)
std::string LoadOne(LuauSandbox &sandbox, const PackageSource &package, const Manifest &manifest, const std::set< std::string > &loaded, const PackageLoadOptions &options, PackageSetResult &result)
void MergeEntry(PackageSetResult &result, const std::string &category, const std::string &key, ScriptValue &&value, const std::string &package, MergeMode mode)
std::string FindForbiddenOverrides(const PackageSetResult &result, const Contribution &contribution)
bool OrderStep(std::map< std::string, OrderNode > &nodes, std::vector< std::string > &order, PackageSetResult &result)
std::expected< Contribution, std::string > RunEntry(LuauSandbox &sandbox, const PackageSource &package, const Manifest &manifest)
std::string TypeCheckReason(std::vector< TypeDiagnostic > &&diagnostics, PackageSetResult &result)
std::expected< Manifest, std::string > ParseManifest(const ScriptValue &table)
std::string ToDebugText(const ScriptValue &value)
std::expected< PackageSource, std::string > ReadPackageFolder(const fs::path &folder)
void AppendCanonicalBytes(const ScriptValue &value, std::string &out)
std::expected< std::vector< PackageSource >, std::string > ReadPackageRoot(const fs::path &root)
uint64_t HashBytes(std::string_view bytes)
std::expected< std::string, std::string > PackageModulePath(std::string_view name)
std::string SummarizeDiagnostics(const std::vector< TypeDiagnostic > &diagnostics, size_t maxShown)
constexpr int64_t PACKAGE_FORMAT_VERSION
std::expected< PackageSetResult, std::string > LoadPackages(LuauSandbox &sandbox, std::span< const PackageSource > packages, const PackageLoadOptions &options)
void AppendCanonicalString(std::string_view text, std::string &out)
uint64_t TableVersion(const PackageSetResult &result)
std::function< std::expected< ModuleFile, std::string >(std::string_view name)> ModuleResolver
std::string TableBytes(const PackageSetResult &result)
bool IsValidPackageName(std::string_view name)
std::vector< TypeDiagnostic > typeDiagnostics
std::vector< TableOverride > overrides
std::vector< std::string > loadOrder
std::vector< PackageRejection > rejected
std::map< std::string, std::map< std::string, TableEntry > > tables
std::map< std::string, std::string > files
std::vector< ScriptField > fields